@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.
- package/CHANGELOG.md +369 -0
- package/dist/checker/check-main.mjs +5 -2
- package/dist/cli.mjs +56 -17
- package/dist/{gateway-api-uhx2l1kC-C2BAxISt.mjs → gateway-api-C0vL3oOK-D24n786A.mjs} +25 -4
- package/dist/gateway.d.mts +2 -2
- package/dist/gateway.mjs +1 -1
- package/docs/deploying.md +7 -1
- package/docs/ingesting.md +10 -5
- package/package.json +1 -1
- package/templates/scaffold/.agents/skills/add-sources/SKILL.md +129 -84
- package/templates/scaffold/.agents/skills/add-sources/verify.mjs +45 -0
- package/templates/scaffold/.agents/skills/format-checker/SKILL.md +18 -46
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +3 -0
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +16 -8
- package/templates/scaffold/.claude/skills/add-sources/SKILL.md +129 -84
- package/templates/scaffold/.claude/skills/add-sources/verify.mjs +45 -0
- package/templates/scaffold/.claude/skills/format-checker/SKILL.md +18 -46
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +3 -0
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +16 -8
- package/templates/scaffold/AGENTS.md +21 -10
- package/templates/scaffold/README.md +47 -16
- package/templates/scaffold/env.example +6 -1
- package/templates/scaffold/gitignore +4 -3
- package/templates/scaffold/system/site/lib/lock.ts +8 -1
- package/templates/scaffold/system/site/lib/people-rule.ts +56 -0
- package/templates/scaffold/system/site/lib/people.ts +5 -24
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +2 -0
- package/templates/scaffold/system/site/record/load.ts +11 -1
- package/templates/scaffold/system/site/record/lock.ts +12 -0
- package/templates/scaffold/.agents/skills/make-slides/SKILL.md +0 -162
- package/templates/scaffold/.agents/skills/make-summary/SKILL.md +0 -153
- package/templates/scaffold/.claude/skills/make-slides/SKILL.md +0 -162
- 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
|
|
115
|
-
documents),
|
|
116
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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. **
|
|
386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3
|
-
#
|
|
4
|
-
# cannot be negated, so the glob is
|
|
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
|
|
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
|
|
12
|
-
* rather than
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|