@panaversity/ksor 0.0.39 → 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 (108) hide show
  1. package/CHANGELOG.md +965 -0
  2. package/README.md +29 -9
  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 +293 -119
  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 +14 -6
  38. package/templates/scaffold/instance.md +21 -17
  39. package/templates/scaffold/knowledge/governance-ladder.md +11 -2
  40. package/templates/scaffold/knowledge/index.md +9 -0
  41. package/templates/scaffold/knowledge/surfaces/for-agents.md +13 -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 +134 -91
  53. package/templates/scaffold/system/site/app/global.css +198 -6
  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/code-block.tsx +87 -0
  59. package/templates/scaffold/system/site/components/embed.tsx +276 -0
  60. package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
  61. package/templates/scaffold/system/site/components/governance.tsx +219 -87
  62. package/templates/scaffold/system/site/components/mdx.tsx +10 -2
  63. package/templates/scaffold/system/site/components/record-index.tsx +5 -5
  64. package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
  65. package/templates/scaffold/system/site/components/record-views.tsx +6 -17
  66. package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
  67. package/templates/scaffold/system/site/lib/alert-rule.ts +214 -0
  68. package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
  69. package/templates/scaffold/system/site/lib/attachments.ts +0 -28
  70. package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
  71. package/templates/scaffold/system/site/lib/audience.ts +42 -146
  72. package/templates/scaffold/system/site/lib/embed-rule.ts +255 -0
  73. package/templates/scaffold/system/site/lib/governance.ts +339 -225
  74. package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
  75. package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
  76. package/templates/scaffold/system/site/lib/lock.ts +282 -0
  77. package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
  78. package/templates/scaffold/system/site/lib/record-href.ts +68 -0
  79. package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
  80. package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
  81. package/templates/scaffold/system/site/lib/shared.ts +67 -104
  82. package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
  83. package/templates/scaffold/system/site/lib/source.ts +256 -186
  84. package/templates/scaffold/system/site/lib/stage-knowledge.ts +638 -467
  85. package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
  86. package/templates/scaffold/system/site/package.json +1 -0
  87. package/templates/scaffold/system/site/record/actor.ts +23 -0
  88. package/templates/scaffold/system/site/record/check.ts +571 -0
  89. package/templates/scaffold/system/site/record/citations.ts +312 -0
  90. package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
  91. package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
  92. package/templates/scaffold/system/site/record/hygiene.ts +320 -0
  93. package/templates/scaffold/system/site/record/index-file.ts +150 -0
  94. package/templates/scaffold/system/site/record/index.ts +103 -0
  95. package/templates/scaffold/system/site/record/instance.ts +257 -0
  96. package/templates/scaffold/system/site/record/instant.ts +43 -0
  97. package/templates/scaffold/system/site/record/ledger.ts +694 -0
  98. package/templates/scaffold/system/site/record/load.ts +129 -0
  99. package/templates/scaffold/system/site/record/lock.ts +306 -0
  100. package/templates/scaffold/system/site/record/near-miss.ts +37 -0
  101. package/templates/scaffold/system/site/record/policy.ts +414 -0
  102. package/templates/scaffold/system/site/record/profile.ts +535 -0
  103. package/templates/scaffold/system/site/record/refusal.ts +106 -0
  104. package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
  105. package/templates/scaffold/system/site/source.config.ts +109 -23
  106. package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
  107. package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
  108. 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,39 +506,82 @@ 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.
452
- - **Each page says how long it takes to read**, just above the body, counted from the
453
- document's own words when the site is built. Fenced code and frontmatter do
454
- not count toward it, so a short page carrying a long example is not reported
455
- as a long read. Nothing to author it is derived. A document with a summary
456
- shows the figure on both tabs, so a reader can see at a glance how much the
457
- summary saves them.
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.
552
+ - **Each page says how long it takes to read**, counted from the document's own
553
+ words when the site is built. Fenced code and frontmatter do not count toward
554
+ it, so a short page carrying a long example is not reported as a long read.
555
+ Nothing to author — it is derived. It sits with the owner and the effective
556
+ date, in the row under the title; a document with a SUMMARY moves it onto
557
+ that view's own strip instead, where it reports the view you are reading
558
+ rather than the document. Each tab names its own figure to a screen reader,
559
+ so the cost of each view is available without switching to it.
458
560
  - **The governance keys are rendered, so they are worth filling in.** Each
459
561
  page shows its owner and effective date under the title, lists every
460
- `provenance` entry separately at the foot, and — for a superseded document —
562
+ `sources` entry separately at the foot, and — for a deprecated document —
461
563
  carries a notice above the title naming its successor and linking to it. A
462
564
  key you leave off renders nothing at all: the site never invents a value, so
463
565
  a missing owner reads as missing rather than as unowned.
464
- - **The agent surface carries them too.** `llms.txt` marks a document whose
465
- status is a caveat and names the route that replaced a superseded one;
466
- `llms-full.txt` puts the keys back as frontmatter above each document. An
467
- agent reading the record therefore sees what a reader sees — a withdrawn
468
- 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.
469
585
  - **Don't want any of it on the published pages?** Set `governance: false`
470
586
  under `site:` in `instance.md`. The record keeps every key — the agent
471
587
  surface and your audit trail still read them, and `llms.txt`/`llms-full.txt`
@@ -473,38 +589,38 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
473
589
  knowing: the home page shows the agent surface VERBATIM in its panel, so the
474
590
  keys stay visible there even with this off. That panel's whole claim is that
475
591
  it is the bytes an agent is served, and editing them to match a page setting
476
- 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
477
- handed a replaced document with no word of its successor has been misled.
478
- - **`status` is shown only when it is a caveat.** `draft`, `review` and
479
- `superseded` appear as a small label; `approved` shows nothing, because a
480
- reader already assumes a document in the record is current so the label
481
- stays rare enough to be noticed on the pages where it matters.
482
- - `visibility:` names the one audience a document belongs to a single value
483
- from `instance.md`'s `audiences:`, never a list, and orthogonal to `status:`
484
- (an approved document can be restricted, and a draft is not hidden). Leave
485
- it off and the document takes `default_visibility`. Using the key WITHOUT
486
- an `audiences:` block is refused on both surfaces `pnpm build` stops with
487
- `ksor-visibility-without-audiences` and `pnpm serve` refuses to boot
488
- because a document marked restricted while nothing enforces it is the one
489
- shape where the frontmatter is the only trace of a restriction that is not
490
- happening. Once `audiences:` is declared, `pnpm check`
491
- refuses any link or `superseded_by:` pointing from a wider audience at a
492
- narrower one the leak no single build can catch, because the build that
493
- 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.
494
611
 
495
612
  **Publication, not authorship: anyone who can clone the repository reads
496
613
  every document regardless of frontmatter; if someone must not read a
497
614
  document and can clone, the answer is a second repository.**
498
615
 
499
- - A replaced document is marked `status: superseded` with `superseded_by:`
500
- pointing at its successor superseded documents are never deleted. The two
501
- keys are one statement, so `pnpm check` refuses each without the other: a
502
- successor pointer left on a document you have set back to `approved` would
503
- publish a "Superseded" banner over a live document. The pointer must name a
504
- markdown document (`./<successor>.md`), exactly as it is capitalised under
505
- `knowledge/`, and it must lead somewhere: a document that supersedes itself,
506
- or a pair that supersede each other, sends the reader in a circle and is
507
- 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`).
508
624
  - Images and assets live in `knowledge/` beside the document that uses them,
509
625
  referenced by relative links. A relative link must never leave `knowledge/`.
510
626
  - **Study attachments.** A document may carry four optional companions named
@@ -520,8 +636,9 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
520
636
 
521
637
  An attachment is **part of its document**, not a document. It has no URL of
522
638
  its own, no sidebar row, no line in `llms.txt`, and no identity an agent can
523
- cite — so it carries **no frontmatter at all** (the checker refuses any), and
524
- 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
525
642
  document and its summary and deck go with it; there is no way to publish a
526
643
  summary more widely than the document it summarises. An attachment whose
527
644
  document is missing is refused, by `pnpm check` and by `pnpm build` alike.
@@ -582,9 +699,9 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
582
699
  from at random and offers another round; a smaller one is simply asked in
583
700
  full.
584
701
 
585
- **`pnpm check` and `pnpm build` both refuse a quiz a reader could pass
586
- without reading**, and this is worth knowing before you write one, because
587
- 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:
588
705
 
589
706
  | Refusal | Means |
590
707
  | -------------------------- | ----------------------------------------------------- |
@@ -664,15 +781,71 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
664
781
  renders in the site's accent. The list lives in `system/site/app/global.css`
665
782
  and is yours — adding a tool is one colour and one mark.
666
783
 
784
+ - **A passage the reader must not miss, as a CALLOUT.** GitHub's alert syntax —
785
+ a blockquote whose first line is the kind in brackets:
786
+
787
+ ```markdown
788
+ > [!WARNING]
789
+ > This threshold changed in March. The figure below is the old one.
790
+ ```
791
+
792
+ Five kinds: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`. The site tints
793
+ the panel in that kind's colour and rules its left edge; every other reader
794
+ of the record — GitHub, a plain editor, `/md/`, `llms-full.txt` — shows an
795
+ ordinary blockquote carrying a visible label, so nothing is lost and nothing
796
+ becomes punctuation.
797
+
798
+ **Not `:::warning`.** That form is a dialect: a record written in one renders
799
+ as literal colons in every reader except the one site that understands it.
800
+
801
+ - **A long line is the reader's to unwrap.** Nothing to author — a fenced block
802
+ wider than the column gets a button beside its copy button that wraps it, and
803
+ a block that fits gets no button at all.
804
+ - **Something running, as an embed.** A document that wants to show a page in
805
+ motion — a simulation, a player, a dashboard — links to it and gives the link
806
+ the title `embed`:
807
+
808
+ ```markdown
809
+ [Play run-until-done](goal-loop.sim.html "embed")
810
+ ```
811
+
812
+ Still CommonMark: a link title is a tooltip everywhere else, so GitHub, a
813
+ plain editor, `/md/` and `llms-full.txt` all show the author's link. Nothing
814
+ loads until a reader clicks, which is what keeps a built page free of
815
+ external requests — and the panel names what it is about to reach, so the
816
+ click is informed.
817
+
818
+ **You do not state a height.** A page carried in the record is measured, so
819
+ the frame is exactly as tall as what it holds — on this record's own seven,
820
+ to the pixel. A number written into a document would be a number some other
821
+ measure makes wrong.
822
+
823
+ **Carry the page in where you can.** A file named `<name>.sim.html`, sitting
824
+ beside its document exactly like a figure, is published by the build and
825
+ served from this site — so it works offline, tells nobody outside what
826
+ someone is reading, and is versioned with the document instead of changing
827
+ under it. It is an ASSET, not an attachment: named freely, as many per
828
+ document as the prose needs.
829
+
830
+ An `https:` link works too, for a page you cannot carry. It is the weaker
831
+ option for a reason worth knowing: many hosts send `X-Frame-Options:
832
+ SAMEORIGIN`, which forbids any other site from framing them, and a browser
833
+ enforces that whatever this record does. Check before you rely on one.
834
+
667
835
  - Copy load-bearing values (numbers, thresholds, dates) exactly from their
668
- 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`.
669
838
 
670
839
  ### Structuring the record
671
840
 
672
- - A folder per topic; its front page is `<folder>/index.md`, and the folder
673
- 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.
674
845
  - Sidebar position is the governed `order:` key: documents that declare it come
675
- 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.
676
849
  - One order drives every surface — the sidebar, `llms.txt`, the home page's
677
850
  first-document link, and the MCP `outline` tool an agent reads to decide what
678
851
  to read first. Set it once and they agree. The door picks up a reorder at the
@@ -687,8 +860,9 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
687
860
  - `.agents/skills/add-sources/` — turn source material (documents, pages,
688
861
  notes) into governed knowledge.
689
862
  - `.agents/skills/make-slides/` — generate a presentation from one document
690
- - `.agents/skills/make-summary/` — write a document's summary and attach it
691
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.
692
866
  - `.agents/skills/format-checker/` — the rules above, as a program;
693
867
  `pnpm check` runs it and its errors explain how to fix themselves.
694
868
 
@@ -696,7 +870,7 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
696
870
 
697
871
  You own `system/site/` outright — these are the seams, cheapest first:
698
872
 
699
- - **Display title** — `instance.md`'s body `# H1` (the intake interview
873
+ - **Display title** — `instance.md`'s `title:` key (the intake interview
700
874
  writes it). Headline, navbar, and browser title follow on restart.
701
875
  - **Accent color** — the one brand pair in `system/site/app/global.css`
702
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