@panaversity/ksor 0.0.30 → 0.0.32

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 CHANGED
@@ -1,5 +1,106 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.32
4
+
5
+ ### Patch Changes
6
+
7
+ - 658c496: One instruction per tool, as tabs.
8
+
9
+ A document that has to say the same thing two ways — one command for one agent,
10
+ another for another — can now put each in its own fenced block and give the
11
+ fence a `tab`:
12
+
13
+ ````markdown
14
+ ```bash tab="Claude Code" tab-group="agent"
15
+ curl -fsSL https://claude.ai/install.sh | bash
16
+ ```
17
+
18
+ ```bash tab="OpenCode" tab-group="agent"
19
+ curl -fsSL https://opencode.ai/install | bash
20
+ ```
21
+ ````
22
+
23
+ Consecutive blocks declaring a `tab` become one tab group. **This is still
24
+ CommonMark** — a fence's info string is free text, so any other markdown reader
25
+ shows both blocks one after another, correct and readable, just without the
26
+ picker. Nothing framework-shaped enters `knowledge/`.
27
+
28
+ `tab-group` is the part worth knowing: blocks sharing a group name switch
29
+ together across the whole page, and the choice is remembered for the reader's
30
+ next visit. A document with ten tabbed sections is one decision rather than ten.
31
+
32
+ A tool the site recognises takes its own colour and mark on its tab — Claude
33
+ Code and OpenCode ship known. Anything else renders in the site's own accent,
34
+ which is the right default for tabs that are `npm`/`pnpm` or `US`/`EU`. The list
35
+ lives in `system/site/app/global.css` and is yours to extend or delete.
36
+
37
+ ## 0.0.31
38
+
39
+ ### Patch Changes
40
+
41
+ - c69232d: Adversarial coverage for the MCP door (issue #33), first slice: the governance
42
+ leak sweep and cross-replica snapshot behaviour.
43
+
44
+ **A withdrawn document must not appear in any field of any reachable response.**
45
+ The existing takedown test proves each serving arm behaves at the arms someone
46
+ thought of. This one plants an unguessable marker inside the withdrawn document
47
+ — in its body _and its title_ — and asserts the marker appears nowhere in the
48
+ serialized result, across eighteen request shapes: search by body, by marker, by
49
+ title words, at several limits, keyword search, `topOneScore`, read by
50
+ stable_id / slug / qualified path, and outline at every anchor and page. A leak
51
+ into a field the test has never heard of still fails it.
52
+
53
+ It carries a **positive control**, because every other assertion is a
54
+ not-contains and a probe that could never see the marker would pass them all
55
+ while proving nothing: each shape runs before the takedown and the ones that
56
+ testify are required to have found it first.
57
+
58
+ It also covers the subtlest case, which carries no content at all — `topOneScore`
59
+ feeds the abstention gate, so a withdrawn document scoring there would let a
60
+ record claim coverage on the strength of text it refuses to show.
61
+
62
+ **Cross-replica snapshot tokens**, listed in #33 as "documented, untested" and
63
+ since found on a real deployment: two processes with no `KSOR_SNAPSHOT_KEYS`
64
+ produce tokens neither can verify from the other, and the verdict is `invalid`
65
+ rather than `unknown_key` — the key _id_ matches and only the secret differs,
66
+ which is why the failure is invisible until you read it. Also pins rotation
67
+ (outstanding tokens survive while the old key is listed, and die when it is
68
+ dropped) and cross-deployment refusal.
69
+
70
+ - d96b139: Presentations, as governed attachments of a document.
71
+
72
+ A document in `knowledge/` may now carry `<doc>.slides.yaml`. It renders at the
73
+ top of that document's page — before the prose, because a deck is the shape of
74
+ the thing and gives the detail somewhere to land.
75
+
76
+ **Ask your coding agent and it writes the deck.** `make slides for
77
+ knowledge/expenses/approvals.md` runs the new `make-slides` skill, which reads
78
+ the document whole, writes the slides, checks every claim and every number back
79
+ against it, and tells you what it left out because the document did not support
80
+ it — which is usually how you find out a document has a gap. No browser, no
81
+ third-party tool, no step where a person takes over.
82
+
83
+ **The record owns the deck by default.** `deck:` carries the slides themselves
84
+ and the site renders them, which is the only mode where a presentation is
85
+ governed: reviewed in the same pull request as its document, versioned with it,
86
+ withdrawn when it is withdrawn, and incapable of rotting into a dead link. Every
87
+ slide ships in the server-rendered HTML, so a reader without JavaScript, a
88
+ crawler and an agent parsing the page all get the whole deck. Presenter notes
89
+ render outside the slide, so they are not projected in fullscreen.
90
+
91
+ **A deck you keep elsewhere** can be embedded instead — `slides.url:`, with the
92
+ embed url derived for Google Slides, Canva and SlideShare. Its frame is
93
+ click-to-load: nothing is requested from the host until a reader asks for it, so
94
+ a page still makes zero external requests and a reader who only wanted the
95
+ policy never announces that to a slide host. Declaring both modes is refused
96
+ (`ksor-slides-two-sources`) — two presentations with nothing to say which one
97
+ governs is the disagreement a system of record exists to settle. `http` urls are
98
+ refused too, since a browser blocks a mixed-content frame silently.
99
+
100
+ Like every attachment, a deck has no URL, no sidebar row, no `llms.txt` line and
101
+ no id an agent can cite, and it takes its `visibility:` and any takedown from its
102
+ parent.
103
+
3
104
  ## 0.0.30
4
105
 
5
106
  ### Patch Changes
package/README.md CHANGED
@@ -32,6 +32,30 @@ scaffold is deterministic and offline, and every refusal explains itself.
32
32
  > and the released version number are authoritative for the exact released
33
33
  > functionality.
34
34
 
35
+ ### Study attachments
36
+
37
+ A document may carry companions named after it, and the site renders each on
38
+ that document's page and nowhere else:
39
+
40
+ | File | What it is |
41
+ | ----------------------- | ------------------------------------------------------ |
42
+ | `<doc>.summary.md` | a précis, shown as a second tab beside the document |
43
+ | `<doc>.slides.yaml` | a presentation, at the top of the page |
44
+ | `<doc>.flashcards.yaml` | a recall deck, at the end |
45
+ | `<doc>.quiz.yaml` | a multiple-choice check, at the end |
46
+
47
+ Ask your coding agent — `make slides for knowledge/expenses/approvals.md` —
48
+ and the `make-slides` skill writes the deck from the document, checks every
49
+ claim and number back against it, and reports what it left out because the
50
+ document did not support it.
51
+
52
+ An attachment is **part of its document**: no URL, no sidebar row, no
53
+ `llms.txt` line, and no id an agent can cite. It takes its `visibility:` and
54
+ any takedown from its parent, so restricting the document restricts them all.
55
+ A quiz whose answers are guessable is refused by the build, and because ingest
56
+ creates no node for an attachment, a quiz's answer key can never reach the
57
+ agent surface at all.
58
+
35
59
  Full concept, design goals, and project status:
36
60
  **<https://github.com/panaversity/ksor>**
37
61
 
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as resolveCommand, r as verbs, t as exitCodes } from "./src-pl4aOpVs.mjs";
3
- import { A as runProbe$1, C as keyRingFromEnv, D as prewarmPool, E as pooledEndpointFor, F as withPgRetry$1, I as withProbeDeadline$1, L as z$1, M as storedTextSearchConfig, N as tlsPosture, O as readHandler, P as visibleTiers$1, S as embedQueryVlit, T as parseInstanceText$1, _ as buildShippedProvider$1, a as MAX_OUTLINE_LIMIT, b as contentPool$1, c as MissingProviderKeyError$1, d as READ_OUTPUT, f as SEARCH_OUTPUT, g as assertSchemaCompatible, h as assertGovernanceServable$1, i as GovernanceGateError$1, j as searchHandler, k as recordIsUndescribed, l as OUTLINE_OUTPUT, m as TextSearchConfigMismatch, n as EmbeddingSpaceMismatch$1, p as SchemaVersionError, r as FLOOR, s as McpServer$1, t as ContentStoreError$1, u as READ_ONLY, v as checkEmbeddingSpace$1, w as outlineHandler, x as contentPoolMin, y as composeInstructions } from "./gateway-api-CbFkHZiU-HvlJRjRB.mjs";
3
+ import { A as runProbe$1, C as keyRingFromEnv, D as prewarmPool, E as pooledEndpointFor, F as withPgRetry$1, I as withProbeDeadline$1, L as z$1, M as storedTextSearchConfig, N as tlsPosture, O as readHandler, P as visibleTiers$1, S as embedQueryVlit, T as parseInstanceText$1, _ as buildShippedProvider$1, a as MAX_OUTLINE_LIMIT, b as contentPool$1, c as MissingProviderKeyError$1, d as READ_OUTPUT, f as SEARCH_OUTPUT, g as assertSchemaCompatible, h as assertGovernanceServable$1, i as GovernanceGateError$1, j as searchHandler, k as recordIsUndescribed, l as OUTLINE_OUTPUT, m as TextSearchConfigMismatch, n as EmbeddingSpaceMismatch$1, p as SchemaVersionError, r as FLOOR, s as McpServer$1, t as ContentStoreError$1, u as READ_ONLY, v as checkEmbeddingSpace$1, w as outlineHandler, x as contentPoolMin, y as composeInstructions } from "./gateway-api-6nC9x54K-BWFTI_6U.mjs";
4
4
  import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { InMemoryTransport, LATEST_PROTOCOL_VERSION, createMcpHandler } from "@modelcontextprotocol/server";
@@ -16,7 +16,7 @@ import { bodyLimit } from "hono/body-limit";
16
16
  import { execFileSync, spawnSync } from "node:child_process";
17
17
  import { parseArgs } from "node:util";
18
18
  import { readFile, readdir, stat } from "node:fs/promises";
19
- //#region ../content-gateway/dist/main-CS5a_8OW.mjs
19
+ //#region ../content-gateway/dist/main-DdIuyneg.mjs
20
20
  /**
21
21
  * The default registration — and the ORIGINAL of the file `ksor init` emits.
22
22
  *
@@ -1583,7 +1583,7 @@ async function withPgRetry(op, options = {}) {
1583
1583
  throw lastError;
1584
1584
  }
1585
1585
  //#endregion
1586
- //#region ../content/dist/commands-CLUS9CX8.mjs
1586
+ //#region ../content/dist/commands-mZxv9bLG.mjs
1587
1587
  /**
1588
1588
  * EVAL-LOCKED constants, quarried verbatim from the oracle
1589
1589
  * (sor-agentfactory @ b554f91, config.py) — changing any of these is a
@@ -4212,6 +4212,10 @@ const ATTACHMENT_SUFFIXES = [
4212
4212
  {
4213
4213
  suffix: ".quiz.yaml",
4214
4214
  kind: "quiz"
4215
+ },
4216
+ {
4217
+ suffix: ".slides.yaml",
4218
+ kind: "slides"
4215
4219
  }
4216
4220
  ];
4217
4221
  /**
@@ -5,7 +5,7 @@ import { z, z as z$1 } from "zod";
5
5
  import pg from "pg";
6
6
  import path from "node:path";
7
7
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
8
- //#region ../content-gateway/dist/gateway-api-CbFkHZiU.mjs
8
+ //#region ../content-gateway/dist/gateway-api-6nC9x54K.mjs
9
9
  /**
10
10
  * The record's system prompt: instance.md's body, preserved beneath a framework
11
11
  * floor. Lifted out of server.ts so the public gateway surface can re-export
package/dist/gateway.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { L as z$1, O as readHandler, a as MAX_OUTLINE_LIMIT, d as READ_OUTPUT, f as SEARCH_OUTPUT, j as searchHandler, k as recordIsUndescribed, l as OUTLINE_OUTPUT, o as MAX_SEARCH_K, r as FLOOR, s as McpServer$1, u as READ_ONLY, w as outlineHandler, y as composeInstructions } from "./gateway-api-CbFkHZiU-HvlJRjRB.mjs";
1
+ import { L as z$1, O as readHandler, a as MAX_OUTLINE_LIMIT, d as READ_OUTPUT, f as SEARCH_OUTPUT, j as searchHandler, k as recordIsUndescribed, l as OUTLINE_OUTPUT, o as MAX_SEARCH_K, r as FLOOR, s as McpServer$1, u as READ_ONLY, w as outlineHandler, y as composeInstructions } from "./gateway-api-6nC9x54K-BWFTI_6U.mjs";
2
2
  export { FLOOR, MAX_OUTLINE_LIMIT, MAX_SEARCH_K, McpServer$1 as McpServer, OUTLINE_OUTPUT, READ_ONLY, READ_OUTPUT, SEARCH_OUTPUT, composeInstructions, outlineHandler, readHandler, recordIsUndescribed, searchHandler, z$1 as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "description": "Knowledge System of Record — compile governed markdown into a static site for people and an MCP server for AI agents, with citations and measured abstention.",
5
5
  "keywords": [
6
6
  "abstention",
@@ -41,7 +41,13 @@ const ASSET_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".web
41
41
  // rules below. This mirrors packages/content/src/lib/attachment-rule.ts, which
42
42
  // this dependency-free file cannot import; ATTACHMENT_CASES there is the table
43
43
  // both are held to.
44
- const ATTACHMENT_SUFFIXES = [".summary.md", ".summary.mdx", ".flashcards.yaml", ".quiz.yaml"];
44
+ const ATTACHMENT_SUFFIXES = [
45
+ ".summary.md",
46
+ ".summary.mdx",
47
+ ".flashcards.yaml",
48
+ ".quiz.yaml",
49
+ ".slides.yaml",
50
+ ];
45
51
  // One character off a real attachment, refused BY NAME: `.yml` reaches the
46
52
  // site bundler's `Unknown file type` throw, which names the path and nothing
47
53
  // about the rule.
@@ -51,6 +57,8 @@ const ATTACHMENT_NEAR_MISSES = [
51
57
  [".summary.markdown", ".summary.md"],
52
58
  [".quiz.yml", ".quiz.yaml"],
53
59
  [".quiz.json", ".quiz.yaml"],
60
+ [".slides.yml", ".slides.yaml"],
61
+ [".slides.json", ".slides.yaml"],
54
62
  ];
55
63
 
56
64
  /** The attachment suffix this name carries, or null. A dotfile has no stem. */
@@ -0,0 +1,160 @@
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** — `owner`, `effective`, `status` from the frontmatter
42
+
43
+ If the document carries `<doc>.summary.md`, read that too: it is a reviewed
44
+ compression of the same thing, and it tells you what the author thought was
45
+ load-bearing.
46
+
47
+ ## 2 · Write the deck
48
+
49
+ Write `<doc>.slides.yaml` beside the document:
50
+
51
+ ```yaml
52
+ slides:
53
+ title: Expense approvals
54
+ description: The 15-minute version, for a room.
55
+ deck:
56
+ - heading: What this settles
57
+ lead: One sentence, in the document's own words.
58
+ note: What to say here. Spoken, never shown.
59
+
60
+ - heading: The rule
61
+ bullets:
62
+ - Two approvers above the threshold, always
63
+ - The threshold is per invoice, including tax
64
+ note: Pause here. This is the slide people remember wrong.
65
+ ```
66
+
67
+ **Per slide:**
68
+
69
+ | Field | Use |
70
+ | --------- | ------------------------------------------------------------ |
71
+ | `heading` | required — a statement, not a label. "The rule", not "Rules" |
72
+ | `lead` | one sentence, for a slide making a single point |
73
+ | `bullets` | three to five. Six is the cap, and six is already too many |
74
+ | `note` | what the presenter SAYS — never a repeat of the slide |
75
+
76
+ **Per deck** — 8 to 14 slides for an ordinary policy document:
77
+
78
+ 1. What this settles, and for whom
79
+ 2. Why it exists — the decision behind it
80
+ 3. The rule itself, stated once
81
+ 4. One slide per case, with the real numbers
82
+ 5. What people get wrong, and what is true instead
83
+ 6. The boundary — what this document does not cover
84
+ 7. Where to find it: the route, the owner, the effective date
85
+
86
+ **Habits that decide whether it is any good:**
87
+
88
+ - **A heading is a claim.** "Recency is not authority" teaches; "Authority"
89
+ does not.
90
+ - **A bullet is one thought.** If it needs a comma splice, it is two bullets.
91
+ - **The note carries the argument.** The slide holds the shape; the presenter
92
+ holds the reasoning. A note repeating the bullets is a wasted field.
93
+ - **Do not pad to a target.** Five slides of substance beat twelve with three
94
+ that exist to reach twelve.
95
+
96
+ ## 3 · Check every line against the document
97
+
98
+ Go back through slide by slide with the document open. For each:
99
+
100
+ - Is every claim in the document? Name where.
101
+ - Is every number identical, same units, same rounding?
102
+ - Does any slide imply a rule the document does not state?
103
+ - Does the boundary slide match what the document actually excludes?
104
+
105
+ This pass finds real errors, reliably. A transcription slip in a deck outlives
106
+ the session it was made for, because the next presenter trusts it.
107
+
108
+ ## 4 · Verify it
109
+
110
+ ```sh
111
+ pnpm check # refuses an orphan, frontmatter, or a malformed deck
112
+ pnpm dev # look at the page — the deck renders at the end
113
+ ```
114
+
115
+ `pnpm build` refuses:
116
+
117
+ - `ksor-slides-empty` — neither `deck:` nor `slides.url:`; nothing to show
118
+ - `ksor-slides-two-sources` — both, so nothing says which one governs
119
+ - `ksor-attachment-orphan` — no `<doc>.md` beside it
120
+ - `ksor-attachment-frontmatter` — an attachment carries none of its own
121
+
122
+ ## 5 · Tell the owner what you did
123
+
124
+ Which document, how many slides, and **anything you left out because the
125
+ document did not support it**. That last part is the useful half: it is how an
126
+ owner finds out their document has a gap.
127
+
128
+ ## Embedding a deck made elsewhere
129
+
130
+ If the owner already has a deck in Google Slides, Canva or SlideShare, use the
131
+ linked mode — `slides.url:` and no `deck:`:
132
+
133
+ ```yaml
134
+ slides:
135
+ title: Expense approvals
136
+ url: https://docs.google.com/presentation/d/<id>/edit?usp=sharing
137
+ ```
138
+
139
+ The embed url is derived for those three providers; for anything else add
140
+ `embed:` explicitly or it renders as a link. `url` must be https — a browser
141
+ blocks a mixed-content frame silently, so an http one publishes a panel that
142
+ never loads.
143
+
144
+ **Prefer the record-owned deck.** A linked deck is not reviewed in a pull
145
+ request, not versioned with its document, not withdrawn when the document is,
146
+ and can rot to a dead link with nothing going red. Use the link when the deck
147
+ already exists and somebody else maintains it — not as the default.
148
+
149
+ ## What NOT to do
150
+
151
+ - **Do not put the deck in the document.** No `<iframe>`, no raw HTML.
152
+ `knowledge/` is CommonMark and must read cleanly in any markdown viewer.
153
+ - **Do not write a slide the document cannot support**, even a true one. If it
154
+ is not in the record, the record cannot stand behind it.
155
+ - **Do not make one deck for several documents.** A deck belongs to one
156
+ document, the way a summary does. A deck spanning five policies has no
157
+ document to be governed by and nothing to be withdrawn with.
158
+ - **Do not patch a stale deck.** When the document changes materially,
159
+ regenerate from it. Patching is how a deck and its document drift, and a
160
+ deck that drifts starts winning arguments it should lose.
@@ -41,7 +41,13 @@ const ASSET_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".web
41
41
  // rules below. This mirrors packages/content/src/lib/attachment-rule.ts, which
42
42
  // this dependency-free file cannot import; ATTACHMENT_CASES there is the table
43
43
  // both are held to.
44
- const ATTACHMENT_SUFFIXES = [".summary.md", ".summary.mdx", ".flashcards.yaml", ".quiz.yaml"];
44
+ const ATTACHMENT_SUFFIXES = [
45
+ ".summary.md",
46
+ ".summary.mdx",
47
+ ".flashcards.yaml",
48
+ ".quiz.yaml",
49
+ ".slides.yaml",
50
+ ];
45
51
  // One character off a real attachment, refused BY NAME: `.yml` reaches the
46
52
  // site bundler's `Unknown file type` throw, which names the path and nothing
47
53
  // about the rule.
@@ -51,6 +57,8 @@ const ATTACHMENT_NEAR_MISSES = [
51
57
  [".summary.markdown", ".summary.md"],
52
58
  [".quiz.yml", ".quiz.yaml"],
53
59
  [".quiz.json", ".quiz.yaml"],
60
+ [".slides.yml", ".slides.yaml"],
61
+ [".slides.json", ".slides.yaml"],
54
62
  ];
55
63
 
56
64
  /** The attachment suffix this name carries, or null. A dotfile has no stem. */
@@ -0,0 +1,160 @@
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** — `owner`, `effective`, `status` from the frontmatter
42
+
43
+ If the document carries `<doc>.summary.md`, read that too: it is a reviewed
44
+ compression of the same thing, and it tells you what the author thought was
45
+ load-bearing.
46
+
47
+ ## 2 · Write the deck
48
+
49
+ Write `<doc>.slides.yaml` beside the document:
50
+
51
+ ```yaml
52
+ slides:
53
+ title: Expense approvals
54
+ description: The 15-minute version, for a room.
55
+ deck:
56
+ - heading: What this settles
57
+ lead: One sentence, in the document's own words.
58
+ note: What to say here. Spoken, never shown.
59
+
60
+ - heading: The rule
61
+ bullets:
62
+ - Two approvers above the threshold, always
63
+ - The threshold is per invoice, including tax
64
+ note: Pause here. This is the slide people remember wrong.
65
+ ```
66
+
67
+ **Per slide:**
68
+
69
+ | Field | Use |
70
+ | --------- | ------------------------------------------------------------ |
71
+ | `heading` | required — a statement, not a label. "The rule", not "Rules" |
72
+ | `lead` | one sentence, for a slide making a single point |
73
+ | `bullets` | three to five. Six is the cap, and six is already too many |
74
+ | `note` | what the presenter SAYS — never a repeat of the slide |
75
+
76
+ **Per deck** — 8 to 14 slides for an ordinary policy document:
77
+
78
+ 1. What this settles, and for whom
79
+ 2. Why it exists — the decision behind it
80
+ 3. The rule itself, stated once
81
+ 4. One slide per case, with the real numbers
82
+ 5. What people get wrong, and what is true instead
83
+ 6. The boundary — what this document does not cover
84
+ 7. Where to find it: the route, the owner, the effective date
85
+
86
+ **Habits that decide whether it is any good:**
87
+
88
+ - **A heading is a claim.** "Recency is not authority" teaches; "Authority"
89
+ does not.
90
+ - **A bullet is one thought.** If it needs a comma splice, it is two bullets.
91
+ - **The note carries the argument.** The slide holds the shape; the presenter
92
+ holds the reasoning. A note repeating the bullets is a wasted field.
93
+ - **Do not pad to a target.** Five slides of substance beat twelve with three
94
+ that exist to reach twelve.
95
+
96
+ ## 3 · Check every line against the document
97
+
98
+ Go back through slide by slide with the document open. For each:
99
+
100
+ - Is every claim in the document? Name where.
101
+ - Is every number identical, same units, same rounding?
102
+ - Does any slide imply a rule the document does not state?
103
+ - Does the boundary slide match what the document actually excludes?
104
+
105
+ This pass finds real errors, reliably. A transcription slip in a deck outlives
106
+ the session it was made for, because the next presenter trusts it.
107
+
108
+ ## 4 · Verify it
109
+
110
+ ```sh
111
+ pnpm check # refuses an orphan, frontmatter, or a malformed deck
112
+ pnpm dev # look at the page — the deck renders at the end
113
+ ```
114
+
115
+ `pnpm build` refuses:
116
+
117
+ - `ksor-slides-empty` — neither `deck:` nor `slides.url:`; nothing to show
118
+ - `ksor-slides-two-sources` — both, so nothing says which one governs
119
+ - `ksor-attachment-orphan` — no `<doc>.md` beside it
120
+ - `ksor-attachment-frontmatter` — an attachment carries none of its own
121
+
122
+ ## 5 · Tell the owner what you did
123
+
124
+ Which document, how many slides, and **anything you left out because the
125
+ document did not support it**. That last part is the useful half: it is how an
126
+ owner finds out their document has a gap.
127
+
128
+ ## Embedding a deck made elsewhere
129
+
130
+ If the owner already has a deck in Google Slides, Canva or SlideShare, use the
131
+ linked mode — `slides.url:` and no `deck:`:
132
+
133
+ ```yaml
134
+ slides:
135
+ title: Expense approvals
136
+ url: https://docs.google.com/presentation/d/<id>/edit?usp=sharing
137
+ ```
138
+
139
+ The embed url is derived for those three providers; for anything else add
140
+ `embed:` explicitly or it renders as a link. `url` must be https — a browser
141
+ blocks a mixed-content frame silently, so an http one publishes a panel that
142
+ never loads.
143
+
144
+ **Prefer the record-owned deck.** A linked deck is not reviewed in a pull
145
+ request, not versioned with its document, not withdrawn when the document is,
146
+ and can rot to a dead link with nothing going red. Use the link when the deck
147
+ already exists and somebody else maintains it — not as the default.
148
+
149
+ ## What NOT to do
150
+
151
+ - **Do not put the deck in the document.** No `<iframe>`, no raw HTML.
152
+ `knowledge/` is CommonMark and must read cleanly in any markdown viewer.
153
+ - **Do not write a slide the document cannot support**, even a true one. If it
154
+ is not in the record, the record cannot stand behind it.
155
+ - **Do not make one deck for several documents.** A deck belongs to one
156
+ document, the way a summary does. A deck spanning five policies has no
157
+ document to be governed by and nothing to be withdrawn with.
158
+ - **Do not patch a stale deck.** When the document changes materially,
159
+ regenerate from it. Patching is how a deck and its document drift, and a
160
+ deck that drifts starts winning arguments it should lose.