@panaversity/ksor 0.0.54 → 0.0.56

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 (33) hide show
  1. package/CHANGELOG.md +369 -0
  2. package/dist/checker/check-main.mjs +5 -2
  3. package/dist/cli.mjs +56 -17
  4. package/dist/{gateway-api-uhx2l1kC-C2BAxISt.mjs → gateway-api-C0vL3oOK-D24n786A.mjs} +25 -4
  5. package/dist/gateway.d.mts +2 -2
  6. package/dist/gateway.mjs +1 -1
  7. package/docs/deploying.md +7 -1
  8. package/docs/ingesting.md +10 -5
  9. package/package.json +1 -1
  10. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +129 -84
  11. package/templates/scaffold/.agents/skills/add-sources/verify.mjs +45 -0
  12. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +18 -46
  13. package/templates/scaffold/.agents/skills/format-checker/check.mjs +3 -0
  14. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +16 -8
  15. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +129 -84
  16. package/templates/scaffold/.claude/skills/add-sources/verify.mjs +45 -0
  17. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +18 -46
  18. package/templates/scaffold/.claude/skills/format-checker/check.mjs +3 -0
  19. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +16 -8
  20. package/templates/scaffold/AGENTS.md +21 -10
  21. package/templates/scaffold/README.md +47 -16
  22. package/templates/scaffold/env.example +6 -1
  23. package/templates/scaffold/gitignore +4 -3
  24. package/templates/scaffold/system/site/lib/lock.ts +8 -1
  25. package/templates/scaffold/system/site/lib/people-rule.ts +56 -0
  26. package/templates/scaffold/system/site/lib/people.ts +5 -24
  27. package/templates/scaffold/system/site/lib/stage-knowledge.ts +2 -0
  28. package/templates/scaffold/system/site/record/load.ts +11 -1
  29. package/templates/scaffold/system/site/record/lock.ts +12 -0
  30. package/templates/scaffold/.agents/skills/make-slides/SKILL.md +0 -162
  31. package/templates/scaffold/.agents/skills/make-summary/SKILL.md +0 -153
  32. package/templates/scaffold/.claude/skills/make-slides/SKILL.md +0 -162
  33. package/templates/scaffold/.claude/skills/make-summary/SKILL.md +0 -153
@@ -111,9 +111,9 @@ Before changing anything, get a feel for how the record behaves. With
111
111
  - **Ask your coding agent to do the same.** `AGENTS.md` carries the working
112
112
  rules; read it before you change how documents are written here. The kit in
113
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).
114
+ record with you), `add-sources` (turn what you have — files, or what you
115
+ know — into governed documents), and `format-checker` (the rules, as a program — also what
116
+ `pnpm check` runs).
117
117
 
118
118
  **Treat the starters as scratch paper.** They ship approved by
119
119
  `ksor-starter/KSOR-STAMP-VERSION` — a tool, not a person. Edit the body and that
@@ -174,7 +174,26 @@ the variable name here only if you want a different one.
174
174
  ### 2. Get a database — your agent can do this one
175
175
 
176
176
  `.mcp.json` at the repo root declares the MCP servers this project may reach.
177
- The first is Neon's. With it connected, ask your coding agent:
177
+ It ships with two, and both are yours to keep or delete — it is your file:
178
+
179
+ | server | what it is |
180
+ | -------------------------------- | ------------------------------------------------------------------------------------------------ |
181
+ | `Neon` | Neon's own hosted MCP server, for provisioning the Postgres the agent surface needs (step 2) |
182
+ | `agentfactory-system-of-record` | a KSoR record Panaversity operates, served over MCP — an example of the surface you are building |
183
+
184
+ The second is **not** your record and is not needed to run this project. It is
185
+ read-only and it answers about the Agent Factory curriculum, not about your
186
+ knowledge. Delete the entry if you would rather your agent not have it; nothing
187
+ here depends on it.
188
+
189
+ **Before you connect Neon, know what you are granting.** The Neon MCP server
190
+ acts on your Neon *account*, not on one database: an agent holding it can create
191
+ and delete projects and branches. Point it at an account you are willing to let
192
+ an agent change, review the plan it shows you before approving, and read Neon's
193
+ own documentation on the server's scopes and permissions rather than taking this
194
+ paragraph as the whole of it.
195
+
196
+ With it connected, ask your coding agent:
178
197
 
179
198
  > Using the Neon MCP server, create a project called `<your-record>` and enable
180
199
  > the pgvector extension on it. Then create a branch called `dev`, and save that
@@ -254,17 +273,14 @@ separately](#the-agent-surface-deploys-separately). Any other operation is
254
273
  ### Test the door with an actual agent
255
274
 
256
275
  The MCP door is meant to be read by agents, so check it with one rather than
257
- with `curl`. With `pnpm serve` running, write `.mcp.json` at the repo root:
276
+ with `curl`. With `pnpm serve` running, **add** an entry to the `.mcp.json` you
277
+ already have — alongside `Neon`, not in place of it:
258
278
 
259
279
  ```json
260
- {
261
- "mcpServers": {
262
280
  "test-record": {
263
281
  "type": "http",
264
282
  "url": "http://127.0.0.1:8080/mcp"
265
283
  }
266
- }
267
- }
268
284
  ```
269
285
 
270
286
  **If you skipped `calibrate`, expect answers where this test wants refusals** —
@@ -288,7 +304,9 @@ Question 2 is the one that matters. Anything can answer questions it has the
288
304
  text for; refusing a plausible near-miss is the property that makes a system of
289
305
  record worth trusting, and it is the one that breaks quietly.
290
306
 
291
- Delete `.mcp.json`, or keep it — it holds no secret.
307
+ Keep the `test-record` entry or remove it — it points at loopback and holds no
308
+ secret either way. Do not delete `.mcp.json` itself: it is where `Neon` is
309
+ declared, and step 2 above needs it.
292
310
 
293
311
  ---
294
312
 
@@ -382,11 +400,22 @@ wrong thing describes it on every surface. So replace them, in this order:
382
400
  approve or withdraw a document — then it writes `instance.md` with you and
383
401
  replaces the `human:you` placeholder in `.ksor/governance.yaml` with your
384
402
  real handle.
385
- 2. **Delete each starter document** as your own knowledge arrives.
386
- 3. **When the last one is gone, delete `ksor-starter/KSOR-STAMP-VERSION` from
403
+ 2. **Write and approve at least one document of your own** — ask your agent
404
+ to add it (the `add-sources` skill), read it on `pnpm dev`, then approve it.
405
+ A record is never empty: delete all five starters before this and the next
406
+ build refuses `ksor-record-empty` and writes nothing.
407
+ 3. **Delete each starter document** as your own knowledge arrives.
408
+ 4. **When the last one is gone, delete `ksor-starter/KSOR-STAMP-VERSION` from
387
409
  `approval_authorities` in `.ksor/governance.yaml`.** Nothing of yours should
388
410
  be approved by a tool.
389
411
 
412
+ If you did the hello-world tutorial first, your document is approved by
413
+ `human:you`. The interview replaces that placeholder with your real handle —
414
+ and re-attributes every act already recorded under it to you in the same
415
+ change, because it is the same person. A policy that no longer names
416
+ `human:you` beside a document that still cites it refuses
417
+ `ksor-approver-unauthorised`.
418
+
390
419
  ---
391
420
 
392
421
  ## Writing knowledge
@@ -452,7 +481,7 @@ record:
452
481
  make slides for knowledge/expenses/approvals.md
453
482
  ```
454
483
 
455
- The `make-slides` skill reads the document whole, writes the deck into
484
+ Your agent reads the document whole, writes the deck into
456
485
  `knowledge/expenses/approvals.slides.yaml`, checks every claim and every number
457
486
  back against the document, and tells you what it left out because the document
458
487
  did not support it — which is usually how you find out a document has a gap. The
@@ -476,7 +505,7 @@ way:
476
505
  summarise knowledge/expenses/approvals.md
477
506
  ```
478
507
 
479
- The `make-summary` skill reads the document whole, writes
508
+ Your agent reads the document whole, writes
480
509
  `knowledge/expenses/approvals.summary.md`, and checks every line back against
481
510
  the document — every number, every rule, and every `##` section, because a
482
511
  summary that covers the opening and trails off is worse than none: a reader who
@@ -487,7 +516,7 @@ The summary is part of its document, not a document of its own: no route, no
487
516
  sidebar row, no line in `llms.txt`, and it takes its governance from its parent.
488
517
  Ask for one only where there is something to compress — under about two screens,
489
518
  a summary that restates the page teaches readers the tab is not worth opening,
490
- and the skill will say so rather than write one.
519
+ and a good agent will say so rather than write one.
491
520
 
492
521
  ---
493
522
 
@@ -664,7 +693,7 @@ different coding agent's way of finding the same working contract.
664
693
  | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
665
694
  | `AGENTS.md` | the working contract every coding agent reads first — the rules for writing knowledge here. |
666
695
  | `CLAUDE.md` | one line, pointing at `AGENTS.md`. Claude Code looks for this filename, not that one. |
667
- | `.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). |
696
+ | `.agents/skills/` | the agent kit: `intake-interview` (define the record with you), `add-sources` (turn files, or what you know, into governed documents), `format-checker` (the rules, as a program). |
668
697
  | `.claude/skills/` | byte-identical copies of the kit — Claude Code discovers skills only here. The checker enforces the mirror, so the two cannot drift. |
669
698
  | `.gemini/settings.json` | points Gemini CLI at `AGENTS.md`; Gemini does not read that filename on its own. |
670
699
  | `.github/workflows/validate.yml` | your CI: runs the same checker on every pull request and push to main. |
@@ -693,6 +722,8 @@ map rather than a substitute.
693
722
  | 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 |
694
723
  | 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` |
695
724
  | the home page and `/llms.txt` are empty | every document is still a draft — correct, not broken | approve one and rebuild |
725
+ | `ksor-record-empty` | every document was deleted — a record is never empty, so nothing was written | add one document of your own (or restore one from git) before deleting the last starter |
726
+ | `ksor-approver-unauthorised` | a document is approved by an actor `.ksor/governance.yaml` no longer names — usually `human:you` after the interview | re-attribute the approval to your handle, or restore the actor to `approval_authorities` |
696
727
  | a new document never appears on the built site | drafts reach no built surface at all | publish it — `status: stable` plus both governance keys |
697
728
  | 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 |
698
729
  | Vercel: `no services are declared` | Root Directory was auto-filled with `system/site` | set it to `./` |
@@ -10,8 +10,13 @@
10
10
  # explicitly instead of relying on a driver default that is due to change.
11
11
  KSOR_DB_URL=postgresql://user:password@host:5432/dbname
12
12
 
13
- # The embedding provider key. instance.md defaults to gemini-embedding-001.
13
+ # The embedding provider key — ONE of these, whichever `embedding.provider` in
14
+ # instance.md names. It defaults to gemini, so that is the one uncommented; on
15
+ # `provider: openai`, comment this out and set OPENAI_API_KEY instead. A record
16
+ # reads exactly one of them, and `ksor serve` names the one it wanted when it
17
+ # is missing.
14
18
  GEMINI_API_KEY=
19
+ # OPENAI_API_KEY=
15
20
 
16
21
  # ── Who may ask ─────────────────────────────────────────────────────────────
17
22
  # ONE variable, and its VALUE is the decision. `ksor serve` refuses to boot
@@ -1,7 +1,8 @@
1
1
  # ksor's working directory — build output and scratch, never the record.
2
- # The two governance files inside it ARE the record (the policy and the
3
- # takedown ledger) and are un-ignored by name: the directory form `.ksor/`
4
- # cannot be negated, so the glob is `.ksor/*`.
2
+ # The governance files inside it ARE the record (the policy, the takedown
3
+ # ledger, and the phone book the site publishes names from) and are un-ignored
4
+ # by name: the directory form `.ksor/` cannot be negated, so the glob is
5
+ # `.ksor/*`.
5
6
  .ksor/*
6
7
  !.ksor/governance.yaml
7
8
  !.ksor/people.yaml
@@ -73,6 +73,8 @@ export interface ControlTexts {
73
73
  readonly policy: string;
74
74
  /** Null when `.ksor/takedowns.yaml` is not there — an empty ledger. */
75
75
  readonly ledger: string | null;
76
+ /** Null when `.ksor/people.yaml` is not there — no natural names declared. */
77
+ readonly people: string | null;
76
78
  }
77
79
 
78
80
  function sha256Text(text: string): string {
@@ -139,12 +141,17 @@ export function readLock(
139
141
  ["instance.md", lock.data.instance_sha256, sha256Text(control.instance)],
140
142
  [".ksor/governance.yaml", lock.data.policy_sha256, sha256Text(control.policy)],
141
143
  [".ksor/takedowns.yaml", lock.data.ledger_sha256, sha256Text(control.ledger ?? "")],
144
+ // The phone book publishes NAMES in place of stored actors, so an edit here
145
+ // changes the approver printed on every page. Left out, the site could
146
+ // publish one approver while the `/md/` twin stamped with the same
147
+ // `build_id` published another (review, 2026-09-01).
148
+ [".ksor/people.yaml", lock.data.people_sha256, sha256Text(control.people ?? "")],
142
149
  ] as const) {
143
150
  if (want === have) continue;
144
151
  refuse(
145
152
  "ksor-lock-stale",
146
153
  `${file} changed since ${LOCK_FILE} was written`,
147
- "the lock's build_id is a hash over the record AND the three files that govern it, so a projection under a control file the lock never saw publishes what nothing checked — a denial lifted by deleting a line would otherwise leave the lock valid",
154
+ "the lock's build_id is a hash over the record AND the four files that govern it, so a projection under a control file the lock never saw publishes what nothing checked — a denial lifted by deleting a line would otherwise leave the lock valid",
148
155
  "run `ksor build` again and commit the lock with the change; lift a denial with `ksor takedown --revoke <id>`, never by editing the ledger",
149
156
  );
150
157
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * What a phone book file DECLARES, as a pure function of its text.
3
+ *
4
+ * A LEAF, like `actor-display.ts`: `people.ts` roots itself at the project via
5
+ * `shared.ts`, which reads `instance.md` at module load, so a rule living
6
+ * there could only be exercised by building a whole record on disk first. That
7
+ * is a test nobody writes, and the duplicate-key rule below went four days
8
+ * asserted only by a comment because of it.
9
+ *
10
+ * A map comes back for every malformed shape rather than an error, because a
11
+ * phone book is DECORATION: a record with an unreadable one publishes
12
+ * identifiers, which is exactly what it did before the file existed.
13
+ */
14
+
15
+ import { parseAllDocuments } from "yaml";
16
+
17
+ /**
18
+ * The phone book a file DECLARES, as a pure function of its text.
19
+ *
20
+ * Separated from the read so the rule can be exercised without a record on
21
+ * disk: the loader below is a filesystem act rooted at `projectRoot`, and a
22
+ * test that has to build a whole scaffold to ask "what do two entries for one
23
+ * actor mean?" is a test nobody writes. A map is handed back for every
24
+ * malformed shape rather than an error, because a phone book is decoration:
25
+ * a record with an unreadable one publishes identifiers, which is what it did
26
+ * before the file existed.
27
+ */
28
+ export function parsePeople(text: string): ReadonlyMap<string, string> {
29
+ try {
30
+ const docs = parseAllDocuments(text.replace(/^\ufeff/, ""), {
31
+ schema: "core",
32
+ uniqueKeys: true,
33
+ logLevel: "silent",
34
+ });
35
+ const doc = docs[0];
36
+ if (doc === undefined) return new Map();
37
+ // `uniqueKeys: true` RECORDS a duplicate rather than refusing one — `toJS()`
38
+ // still resolves last-wins. Reading the option and not the errors is what
39
+ // made the claim above false: two entries for one actor silently published
40
+ // the second person's name on the first person's governance act.
41
+ if (doc.errors.length > 0) return new Map();
42
+ const value: unknown = doc.toJS();
43
+ if (typeof value !== "object" || value === null) return new Map();
44
+ const table = (value as { people?: unknown }).people;
45
+ if (typeof table !== "object" || table === null || Array.isArray(table)) return new Map();
46
+ const out = new Map<string, string>();
47
+ for (const [actor, name] of Object.entries(table as Record<string, unknown>)) {
48
+ // A blank value is an entry someone started and left; printing "" would
49
+ // erase the identifier rather than replace it.
50
+ if (typeof name === "string" && name.trim() !== "") out.set(actor.trim(), name.trim());
51
+ }
52
+ return out;
53
+ } catch {
54
+ return new Map();
55
+ }
56
+ }
@@ -8,8 +8,9 @@
8
8
  * `human:mjs` — most of the actors in a real record — had no expressible name
9
9
  * at all. It also collided: "Bashir Aziz" and "Bashira Ziz" both derive
10
10
  * `bashiraziz`, which would print one person's name on the other's governance
11
- * act. A map has neither problem, and duplicate keys are refused by the parser
12
- * rather than resolved by whichever came last.
11
+ * act. A map has neither problem, and a duplicate key drops the whole book
12
+ * rather than resolving to whichever came last — `uniqueKeys` only REPORTS the
13
+ * duplicate, so the errors have to be read for that to be true.
13
14
  *
14
15
  * ONE-WAY. The identifier is what the record stores, cites and checks against
15
16
  * the policy; this is only what a page prints. Nothing reads a name back into
@@ -34,8 +35,7 @@
34
35
  import { readFileSync } from "node:fs";
35
36
  import path from "node:path";
36
37
 
37
- import { parseAllDocuments } from "yaml";
38
-
38
+ import { parsePeople } from "./people-rule";
39
39
  import { projectRoot } from "./shared";
40
40
 
41
41
  const PEOPLE_YAML = path.join(projectRoot, ".ksor", "people.yaml");
@@ -48,26 +48,7 @@ function loadPeople(): ReadonlyMap<string, string> {
48
48
  // Optional: its absence means "no natural names declared".
49
49
  return new Map();
50
50
  }
51
- try {
52
- const docs = parseAllDocuments(text.replace(/^/, ""), {
53
- schema: "core",
54
- uniqueKeys: true,
55
- logLevel: "silent",
56
- });
57
- const value: unknown = docs[0]?.toJS();
58
- if (typeof value !== "object" || value === null) return new Map();
59
- const table = (value as { people?: unknown }).people;
60
- if (typeof table !== "object" || table === null || Array.isArray(table)) return new Map();
61
- const out = new Map<string, string>();
62
- for (const [actor, name] of Object.entries(table as Record<string, unknown>)) {
63
- // A blank value is an entry someone started and left; printing "" would
64
- // erase the identifier rather than replace it.
65
- if (typeof name === "string" && name.trim() !== "") out.set(actor.trim(), name.trim());
66
- }
67
- return out;
68
- } catch {
69
- return new Map();
70
- }
51
+ return parsePeople(text);
71
52
  }
72
53
 
73
54
  let cached: ReadonlyMap<string, string> | null = null;
@@ -48,6 +48,7 @@ import type { Refusal } from "../record/refusal";
48
48
  const KNOWLEDGE = "knowledge/";
49
49
  const LEDGER_PATH = ".ksor/takedowns.yaml";
50
50
  const POLICY_PATH = ".ksor/governance.yaml";
51
+ const PEOPLE_PATH = ".ksor/people.yaml";
51
52
  /**
52
53
  * Everything this build may publish, as bytes at bundle-relative paths: the
53
54
  * admitted concepts (copied), their companions (copied), ONLY the assets those
@@ -177,6 +178,7 @@ function planStage(recordDir: string, development: boolean): StagePlan {
177
178
  instance: record.files.get("instance.md") ?? "",
178
179
  policy: record.files.get(POLICY_PATH) ?? "",
179
180
  ledger: record.files.get(LEDGER_PATH) ?? null,
181
+ people: record.files.get(PEOPLE_PATH) ?? null,
180
182
  },
181
183
  { draftsRequested },
182
184
  );
@@ -11,7 +11,17 @@ import path from "node:path";
11
11
  import type { RecordFiles } from "./check";
12
12
  import type { ScaffoldStructure } from "./hygiene";
13
13
 
14
- const CONTROL_FILES = ["instance.md", ".ksor/governance.yaml", ".ksor/takedowns.yaml"] as const;
14
+ // `.ksor/people.yaml` is here because the SITE reads it and publishes what it
15
+ // says — it rewrites the approver, owner and verifier printed on every document
16
+ // page. A file that changes published bytes has to reach `build_id`, or the
17
+ // human surface and the machine surface of one build can disagree about who
18
+ // approved a document with nothing going red (found by review, 2026-09-01).
19
+ const CONTROL_FILES = [
20
+ "instance.md",
21
+ ".ksor/governance.yaml",
22
+ ".ksor/people.yaml",
23
+ ".ksor/takedowns.yaml",
24
+ ] as const;
15
25
  /** Files the operating system writes behind the author's back: ignored, never reported. */
16
26
  const OS_JUNK = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
17
27
 
@@ -46,6 +46,7 @@ const lockSchema = z
46
46
  drafts: z.enum(["hidden", "shown"]),
47
47
  instance_sha256: hex64,
48
48
  policy_sha256: hex64,
49
+ people_sha256: hex64,
49
50
  ledger_sha256: hex64,
50
51
  ledger_entries: z.array(z.object({ id: z.string().min(1), digest: hex64 }).strict()),
51
52
  audiences: z
@@ -89,6 +90,8 @@ export interface Lock {
89
90
  readonly drafts: Drafts;
90
91
  readonly instance_sha256: string;
91
92
  readonly policy_sha256: string;
93
+ /** The phone book the SITE publishes names from. Empty string when absent. */
94
+ readonly people_sha256: string;
92
95
  readonly ledger_sha256: string;
93
96
  /** `(id, digest)` per ledger entry, sorted by id — the baseline the next build compares TEXT against. */
94
97
  readonly ledger_entries: readonly { readonly id: string; readonly digest: string }[];
@@ -154,6 +157,7 @@ export interface BuildIdInputs {
154
157
  readonly indexes: readonly { readonly path: string; readonly sha256: string }[];
155
158
  readonly instance_sha256: string;
156
159
  readonly policy_sha256: string;
160
+ readonly people_sha256: string;
157
161
  readonly ledger_sha256: string;
158
162
  readonly ksor_version: string;
159
163
  readonly drafts: Drafts;
@@ -181,6 +185,9 @@ export function buildIdOf(inputs: BuildIdInputs): string {
181
185
  .sort((a, b) => compare(a[0] ?? "", b[0] ?? "")),
182
186
  instance_sha256: inputs.instance_sha256,
183
187
  policy_sha256: inputs.policy_sha256,
188
+ // The site prints what this file says in place of the stored actor, so it
189
+ // is published content and belongs in the id like any other.
190
+ people_sha256: inputs.people_sha256,
184
191
  ledger_sha256: inputs.ledger_sha256,
185
192
  ksor_version: inputs.ksor_version,
186
193
  drafts: inputs.drafts,
@@ -233,6 +240,8 @@ export interface LockInput {
233
240
  readonly drafts: Drafts;
234
241
  readonly instanceText: string;
235
242
  readonly policyText: string;
243
+ /** Null when `.ksor/people.yaml` does not exist, which is every record until an owner writes one. */
244
+ readonly peopleText: string | null;
236
245
  /** Null when the ledger file does not exist. */
237
246
  readonly ledgerText: string | null;
238
247
  readonly ledgerEntries: readonly { readonly id: string; readonly digest: string }[];
@@ -269,6 +278,7 @@ export function composeLock(input: LockInput): Lock {
269
278
  .map((i) => ({ path: i.path, sha256: sha256Hex(i.text) }));
270
279
  const instance_sha256 = sha256Hex(input.instanceText);
271
280
  const policy_sha256 = sha256Hex(input.policyText);
281
+ const people_sha256 = sha256Hex(input.peopleText ?? "");
272
282
  const ledger_sha256 = sha256Hex(input.ledgerText ?? "");
273
283
  return {
274
284
  format: LOCK_FORMAT,
@@ -279,6 +289,7 @@ export function composeLock(input: LockInput): Lock {
279
289
  indexes,
280
290
  instance_sha256,
281
291
  policy_sha256,
292
+ people_sha256,
282
293
  ledger_sha256,
283
294
  ksor_version: input.ksorVersion,
284
295
  drafts: input.drafts,
@@ -291,6 +302,7 @@ export function composeLock(input: LockInput): Lock {
291
302
  drafts: input.drafts,
292
303
  instance_sha256,
293
304
  policy_sha256,
305
+ people_sha256,
294
306
  ledger_sha256,
295
307
  ledger_entries: [...input.ledgerEntries].sort((a, b) => compare(a.id, b.id)),
296
308
  audiences: { registry: [...input.audiences].sort(), viewers },
@@ -1,162 +0,0 @@
1
- ---
2
- name: make-slides
3
- description: Generate a presentation from one document and attach it, so it renders on that document's page. Use when the owner says "make slides for X", "turn this into a deck", "I need to present this", asks for a teaching aid or a slideshow, or when onboarding needs a session rather than a page.
4
- metadata:
5
- version: "2.0.0"
6
- ---
7
-
8
- # Generating a presentation for a document
9
-
10
- You write the slides. Not an outline for somebody else to build — the actual
11
- deck, into `<doc>.slides.yaml`, which the site renders on that document's page.
12
- No browser, no third-party tool, no step where a human takes over.
13
-
14
- Run it end to end: read the document, write the deck, check every line back
15
- against the document, verify it builds. **The check is not optional** — it is
16
- the step that keeps the record's guarantee true.
17
-
18
- ## The one rule everything else serves
19
-
20
- **A slide may only say what the document says.**
21
-
22
- The deck is a way of presenting the record, never a second source. A slide
23
- asserting a threshold the document does not contain is a claim nothing governs
24
- and no agent can cite — and because the deck is an attachment, the record now
25
- stands behind it. Every number, date, name and rule is copied from the
26
- document exactly, units included.
27
-
28
- If the document does not say something you want on a slide, there are two
29
- honest options: leave it out, or tell the owner the document is missing it.
30
- Never a third.
31
-
32
- ## 1 · Read the document whole, first
33
-
34
- Read `<doc>.md` completely before writing anything. Note as you go:
35
-
36
- - **the decision it settles** — the reason it exists
37
- - **the rule, in its own words** — usually one or two sentences
38
- - **the numbers** — thresholds, deadlines, limits, and their units
39
- - **the cases** — what happens in each situation it names
40
- - **the boundary** — what it explicitly does NOT cover
41
- - **its governance** — `status`, `ksor.owner` and `ksor.effective_from` from
42
- the frontmatter
43
-
44
- If the document carries `<doc>.summary.md`, read that too: it is a reviewed
45
- compression of the same thing, and it tells you what the author thought was
46
- load-bearing.
47
-
48
- ## 2 · Write the deck
49
-
50
- Write `<doc>.slides.yaml` beside the document:
51
-
52
- ```yaml
53
- slides:
54
- title: Expense approvals
55
- description: The 15-minute version, for a room.
56
- deck:
57
- - heading: What this settles
58
- lead: One sentence, in the document's own words.
59
- note: What to say here. Spoken, never shown.
60
-
61
- - heading: The rule
62
- bullets:
63
- - Two approvers above the threshold, always
64
- - The threshold is per invoice, including tax
65
- note: Pause here. This is the slide people remember wrong.
66
- ```
67
-
68
- **Per slide:**
69
-
70
- | Field | Use |
71
- | --------- | ------------------------------------------------------------ |
72
- | `heading` | required — a statement, not a label. "The rule", not "Rules" |
73
- | `lead` | one sentence, for a slide making a single point |
74
- | `bullets` | three to five. Six is the cap, and six is already too many |
75
- | `note` | what the presenter SAYS — never a repeat of the slide |
76
-
77
- **Per deck** — 8 to 14 slides for an ordinary policy document:
78
-
79
- 1. What this settles, and for whom
80
- 2. Why it exists — the decision behind it
81
- 3. The rule itself, stated once
82
- 4. One slide per case, with the real numbers
83
- 5. What people get wrong, and what is true instead
84
- 6. The boundary — what this document does not cover
85
- 7. Where to find it: the route, the owner, the effective date
86
-
87
- **Habits that decide whether it is any good:**
88
-
89
- - **A heading is a claim.** "Recency is not authority" teaches; "Authority"
90
- does not.
91
- - **A bullet is one thought.** If it needs a comma splice, it is two bullets.
92
- - **The note carries the argument.** The slide holds the shape; the presenter
93
- holds the reasoning. A note repeating the bullets is a wasted field.
94
- - **Do not pad to a target.** Five slides of substance beat twelve with three
95
- that exist to reach twelve.
96
-
97
- ## 3 · Check every line against the document
98
-
99
- Go back through slide by slide with the document open. For each:
100
-
101
- - Is every claim in the document? Name where.
102
- - Is every number identical, same units, same rounding?
103
- - Does any slide imply a rule the document does not state?
104
- - Does the boundary slide match what the document actually excludes?
105
-
106
- This pass finds real errors, reliably. A transcription slip in a deck outlives
107
- the session it was made for, because the next presenter trusts it.
108
-
109
- ## 4 · Verify it
110
-
111
- ```sh
112
- pnpm check # refuses an orphan deck
113
- pnpm dev # look at the page — the deck renders after the introduction,
114
- # immediately before the first `##` section
115
- ```
116
-
117
- `pnpm build` refuses — the deck's own shape is checked in the site build, not
118
- by `pnpm check`:
119
-
120
- - `ksor-slides-empty` — neither `deck:` nor `slides.url:`; nothing to show
121
- - `ksor-slides-two-sources` — both, so nothing says which one governs
122
- - `ksor-attachment-orphan` — no `<doc>.md` beside it
123
-
124
- ## 5 · Tell the owner what you did
125
-
126
- Which document, how many slides, and **anything you left out because the
127
- document did not support it**. That last part is the useful half: it is how an
128
- owner finds out their document has a gap.
129
-
130
- ## Embedding a deck made elsewhere
131
-
132
- If the owner already has a deck in Google Slides, Canva or SlideShare, use the
133
- linked mode — `slides.url:` and no `deck:`:
134
-
135
- ```yaml
136
- slides:
137
- title: Expense approvals
138
- url: https://docs.google.com/presentation/d/<id>/edit?usp=sharing
139
- ```
140
-
141
- The embed url is derived for those three providers; for anything else add
142
- `embed:` explicitly or it renders as a link. `url` must be https — a browser
143
- blocks a mixed-content frame silently, so an http one publishes a panel that
144
- never loads.
145
-
146
- **Prefer the record-owned deck.** A linked deck is not reviewed in a pull
147
- request, not versioned with its document, not withdrawn when the document is,
148
- and can rot to a dead link with nothing going red. Use the link when the deck
149
- already exists and somebody else maintains it — not as the default.
150
-
151
- ## What NOT to do
152
-
153
- - **Do not put the deck in the document.** No `<iframe>`, no raw HTML.
154
- `knowledge/` is CommonMark and must read cleanly in any markdown viewer.
155
- - **Do not write a slide the document cannot support**, even a true one. If it
156
- is not in the record, the record cannot stand behind it.
157
- - **Do not make one deck for several documents.** A deck belongs to one
158
- document, the way a summary does. A deck spanning five policies has no
159
- document to be governed by and nothing to be withdrawn with.
160
- - **Do not patch a stale deck.** When the document changes materially,
161
- regenerate from it. Patching is how a deck and its document drift, and a
162
- deck that drifts starts winning arguments it should lose.