@panaversity/ksor 0.0.30 → 0.0.31

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,72 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.31
4
+
5
+ ### Patch Changes
6
+
7
+ - c69232d: Adversarial coverage for the MCP door (issue #33), first slice: the governance
8
+ leak sweep and cross-replica snapshot behaviour.
9
+
10
+ **A withdrawn document must not appear in any field of any reachable response.**
11
+ The existing takedown test proves each serving arm behaves at the arms someone
12
+ thought of. This one plants an unguessable marker inside the withdrawn document
13
+ — in its body _and its title_ — and asserts the marker appears nowhere in the
14
+ serialized result, across eighteen request shapes: search by body, by marker, by
15
+ title words, at several limits, keyword search, `topOneScore`, read by
16
+ stable_id / slug / qualified path, and outline at every anchor and page. A leak
17
+ into a field the test has never heard of still fails it.
18
+
19
+ It carries a **positive control**, because every other assertion is a
20
+ not-contains and a probe that could never see the marker would pass them all
21
+ while proving nothing: each shape runs before the takedown and the ones that
22
+ testify are required to have found it first.
23
+
24
+ It also covers the subtlest case, which carries no content at all — `topOneScore`
25
+ feeds the abstention gate, so a withdrawn document scoring there would let a
26
+ record claim coverage on the strength of text it refuses to show.
27
+
28
+ **Cross-replica snapshot tokens**, listed in #33 as "documented, untested" and
29
+ since found on a real deployment: two processes with no `KSOR_SNAPSHOT_KEYS`
30
+ produce tokens neither can verify from the other, and the verdict is `invalid`
31
+ rather than `unknown_key` — the key _id_ matches and only the secret differs,
32
+ which is why the failure is invisible until you read it. Also pins rotation
33
+ (outstanding tokens survive while the old key is listed, and die when it is
34
+ dropped) and cross-deployment refusal.
35
+
36
+ - d96b139: Presentations, as governed attachments of a document.
37
+
38
+ A document in `knowledge/` may now carry `<doc>.slides.yaml`. It renders at the
39
+ top of that document's page — before the prose, because a deck is the shape of
40
+ the thing and gives the detail somewhere to land.
41
+
42
+ **Ask your coding agent and it writes the deck.** `make slides for
43
+ knowledge/expenses/approvals.md` runs the new `make-slides` skill, which reads
44
+ the document whole, writes the slides, checks every claim and every number back
45
+ against it, and tells you what it left out because the document did not support
46
+ it — which is usually how you find out a document has a gap. No browser, no
47
+ third-party tool, no step where a person takes over.
48
+
49
+ **The record owns the deck by default.** `deck:` carries the slides themselves
50
+ and the site renders them, which is the only mode where a presentation is
51
+ governed: reviewed in the same pull request as its document, versioned with it,
52
+ withdrawn when it is withdrawn, and incapable of rotting into a dead link. Every
53
+ slide ships in the server-rendered HTML, so a reader without JavaScript, a
54
+ crawler and an agent parsing the page all get the whole deck. Presenter notes
55
+ render outside the slide, so they are not projected in fullscreen.
56
+
57
+ **A deck you keep elsewhere** can be embedded instead — `slides.url:`, with the
58
+ embed url derived for Google Slides, Canva and SlideShare. Its frame is
59
+ click-to-load: nothing is requested from the host until a reader asks for it, so
60
+ a page still makes zero external requests and a reader who only wanted the
61
+ policy never announces that to a slide host. Declaring both modes is refused
62
+ (`ksor-slides-two-sources`) — two presentations with nothing to say which one
63
+ governs is the disagreement a system of record exists to settle. `http` urls are
64
+ refused too, since a browser blocks a mixed-content frame silently.
65
+
66
+ Like every attachment, a deck has no URL, no sidebar row, no `llms.txt` line and
67
+ no id an agent can cite, and it takes its `visibility:` and any takedown from its
68
+ parent.
69
+
3
70
  ## 0.0.30
4
71
 
5
72
  ### 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.31",
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.
@@ -506,12 +506,15 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
506
506
  refused.
507
507
  - Images and assets live in `knowledge/` beside the document that uses them,
508
508
  referenced by relative links. A relative link must never leave `knowledge/`.
509
- - **Study attachments.** A document may carry three optional companions named
509
+ - **Study attachments.** A document may carry four optional companions named
510
510
  after it, in the same folder: `<doc>.summary.md` (a short précis),
511
- `<doc>.flashcards.yaml` (a recall deck) and `<doc>.quiz.yaml` (a
512
- multiple-choice check). The summary appears as a second tab beside the
513
- document's own words; the deck and the quiz appear at the END of that
514
- document's page. None of them appears anywhere else in the site.
511
+ `<doc>.slides.yaml` (a presentation), `<doc>.flashcards.yaml` (a recall deck)
512
+ and `<doc>.quiz.yaml` (a multiple-choice check). The summary appears as a
513
+ second tab beside the document's own words; the presentation appears at the
514
+ TOP of the page, before the document, because a deck is the shape of the
515
+ thing and gives the detail somewhere to land; the deck and the quiz appear at
516
+ the END, because those are used after reading. None of them appears anywhere
517
+ else in the site.
515
518
 
516
519
  An attachment is **part of its document**, not a document. It has no URL of
517
520
  its own, no sidebar row, no line in `llms.txt`, and no identity an agent can
@@ -595,6 +598,41 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
595
598
  predecessor, where the same mistakes shipped and were found by readers — one
596
599
  quiz had every correct answer in the same position across 451 questions.
597
600
 
601
+ A **presentation** is slides the record owns. Ask your coding agent for one
602
+ rather than writing it by hand — `make slides for knowledge/<path>.md` runs
603
+ the `make-slides` skill, which reads the document, writes the deck, checks
604
+ every claim back against it, and tells you what it left out:
605
+
606
+ ```yaml
607
+ slides:
608
+ title: Expense approvals
609
+ description: The 15-minute version, for a room.
610
+ deck:
611
+ - heading: When two copies disagree, one wins
612
+ bullets:
613
+ - Two approvers above the threshold, always
614
+ - The threshold is per invoice, including tax
615
+ note: Spoken, never shown. Pause here — people remember this one wrong.
616
+ ```
617
+
618
+ `heading` is required and should be a claim rather than a label; `lead` is
619
+ one sentence for a single-point slide; `bullets` caps at six because a slide
620
+ someone reads aloud is a slide nobody listens to; `note` is what the
621
+ presenter SAYS and never appears on the slide, including in fullscreen.
622
+
623
+ The slides render in the page — no third party, no request to anyone, and
624
+ every slide is in the shipped HTML, so a reader without JavaScript still gets
625
+ the whole deck. Because they live in the record they are reviewed in the same
626
+ pull request as the document, versioned with it, and withdrawn with it.
627
+
628
+ **A deck you keep elsewhere** can be pointed at instead — `slides.url:` and
629
+ no `deck:`. The embed url is derived for Google Slides, Canva and SlideShare;
630
+ anything else needs an explicit `embed:` or renders as a link. The url must
631
+ be `https` (a browser blocks a mixed-content frame silently), and the frame
632
+ loads on CLICK, so nothing is requested from the host until a reader asks.
633
+ Declaring both `deck:` and `slides.url:` is refused — two presentations with
634
+ nothing to say which one governs.
635
+
598
636
  - Copy load-bearing values (numbers, thresholds, dates) exactly from their
599
637
  source, and name the source in `provenance`.
600
638
 
@@ -617,6 +655,8 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
617
655
  write `instance.md` together.
618
656
  - `.agents/skills/add-sources/` — turn source material (documents, pages,
619
657
  notes) into governed knowledge.
658
+ - `.agents/skills/make-slides/` — generate a presentation from one document
659
+ and attach it, so it renders on that document's page.
620
660
  - `.agents/skills/format-checker/` — the rules above, as a program;
621
661
  `pnpm check` runs it and its errors explain how to fix themselves.
622
662
 
@@ -23,6 +23,29 @@ versions that bundle corepack. The first `pnpm install` also fetches the
23
23
  `ksor` tool (pinned in `package.json`) and writes it into your lockfile —
24
24
  commit the updated lockfile.
25
25
 
26
+ ### Presenting a document
27
+
28
+ Ask your coding agent for slides and it writes them, from the document, into
29
+ the record:
30
+
31
+ ```
32
+ make slides for knowledge/expenses/approvals.md
33
+ ```
34
+
35
+ The `make-slides` skill reads the document whole, writes the deck into
36
+ `knowledge/expenses/approvals.slides.yaml`, checks every claim and every
37
+ number back against the document, and tells you what it left out because the
38
+ document did not support it — which is usually how you find out a document has
39
+ a gap. The deck then renders at the top of that document's page: click through
40
+ it inline, or **Present** for fullscreen. Presenter notes stay off the screen.
41
+
42
+ The slides live in the record, so they are reviewed in the same pull request
43
+ as the document, versioned with it, and withdrawn when it is withdrawn. There
44
+ is no third party and no link to rot. If you already keep a deck in Google
45
+ Slides, Canva or SlideShare you can point at it instead — `slides.url:` rather
46
+ than `deck:` — and the page will offer it as a link with a frame the reader
47
+ loads on click, so nothing is requested from the host until somebody asks.
48
+
26
49
  ### Serving to agents
27
50
 
28
51
  The record's other surface is an MCP server for AI agents — the same
@@ -128,7 +151,7 @@ different coding agent's way of finding the same working contract.
128
151
  | `instance.md` | what this record is authoritative for; its `name:` is the identity every surface publishes (read at server/build start — restart `pnpm dev` after renaming). This prose IS the agent surface's system prompt — `ksor serve` wires it into the MCP server's instructions. |
129
152
  | `AGENTS.md` | the working contract every coding agent reads first — the rules for writing knowledge here. |
130
153
  | `CLAUDE.md` | one line, pointing at `AGENTS.md`. Claude Code looks for this filename, not that one. |
131
- | `.agents/skills/` | the agent kit: `intake-interview` (define the record with you), `add-sources` (turn source material into governed documents), `format-checker` (the rules, as a program). |
154
+ | `.agents/skills/` | the agent kit: `intake-interview` (define the record with you), `add-sources` (turn source material into governed documents), `make-slides` (generate a presentation from a document and attach it), `format-checker` (the rules, as a program). |
132
155
  | `.claude/skills/` | byte-identical copies of the kit — Claude Code discovers skills only here. The checker enforces the mirror, so the two cannot drift. |
133
156
  | `.gemini/settings.json` | points Gemini CLI at `AGENTS.md`; Gemini does not read that filename on its own. |
134
157
  | `.github/workflows/validate.yml` | your CI: runs the same checker on every pull request and push to main. |