@panaversity/ksor 0.0.29 → 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.
Files changed (30) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +24 -0
  3. package/dist/cli.mjs +11 -3
  4. package/dist/{gateway-api-8lNruq9e-CuohjtoK.mjs → gateway-api-6nC9x54K-BWFTI_6U.mjs} +1 -1
  5. package/dist/gateway.mjs +1 -1
  6. package/package.json +1 -1
  7. package/templates/scaffold/.agents/skills/format-checker/check.mjs +11 -1
  8. package/templates/scaffold/.agents/skills/make-slides/SKILL.md +160 -0
  9. package/templates/scaffold/.claude/skills/format-checker/check.mjs +11 -1
  10. package/templates/scaffold/.claude/skills/make-slides/SKILL.md +160 -0
  11. package/templates/scaffold/AGENTS.md +91 -5
  12. package/templates/scaffold/README.md +24 -1
  13. package/templates/scaffold/knowledge/what-is-a-ksor.quiz.yaml +90 -0
  14. package/templates/scaffold/knowledge/what-is-a-ksor.slides.yaml +65 -0
  15. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +17 -2
  16. package/templates/scaffold/system/site/app/global.css +113 -0
  17. package/templates/scaffold/system/site/components/deck-viewer.tsx +195 -0
  18. package/templates/scaffold/system/site/components/quiz.tsx +321 -0
  19. package/templates/scaffold/system/site/components/slides.tsx +128 -0
  20. package/templates/scaffold/system/site/components/study-aids.tsx +1 -1
  21. package/templates/scaffold/system/site/lib/attachment-rule.ts +14 -0
  22. package/templates/scaffold/system/site/lib/attachments.ts +88 -3
  23. package/templates/scaffold/system/site/lib/deck.ts +3 -12
  24. package/templates/scaffold/system/site/lib/identity.ts +55 -0
  25. package/templates/scaffold/system/site/lib/quiz-audit.ts +306 -0
  26. package/templates/scaffold/system/site/lib/quiz-round.ts +57 -0
  27. package/templates/scaffold/system/site/lib/quiz.ts +84 -0
  28. package/templates/scaffold/system/site/lib/slides-embed.ts +93 -0
  29. package/templates/scaffold/system/site/lib/slides.ts +123 -0
  30. package/templates/scaffold/system/site/source.config.ts +25 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,113 @@
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
+
70
+ ## 0.0.30
71
+
72
+ ### Patch Changes
73
+
74
+ - fbf149b: Quizzes, as governed attachments of a document.
75
+
76
+ A document in `knowledge/` may now carry `<doc>.quiz.yaml` beside its summary
77
+ and its flashcard deck. It renders at the end of the document's page, under the
78
+ deck: choose an option, see immediately whether you were right, and read the
79
+ explanation before moving on. There is no pass mark — a quiz here checks
80
+ understanding of the record, it does not certify anybody — and answers stay in
81
+ the reader's own browser.
82
+
83
+ A quiz is **part of its document, not a document**: no URL, no sidebar row, no
84
+ `llms.txt` line, no markdown twin, no search entry, and no stable id. That last
85
+ one settles a question worth being explicit about: because `ksor ingest` creates
86
+ no node for a quiz, **the answer key cannot reach the MCP surface at all**.
87
+ There is nothing for an agent to search and nothing for it to read — not by a
88
+ filter that could be forgotten, but because the row does not exist. Governance
89
+ inherits from the parent exactly as the summary and the deck already do.
90
+
91
+ **`pnpm check` and `pnpm build` refuse a quiz a reader could pass without
92
+ reading it**, naming the questions to fix:
93
+
94
+ - `ksor-quiz-answer-bias` — more than 60% of answers at one option position
95
+ - `ksor-quiz-length-bias` — picking the longest or shortest option usually wins
96
+ - `ksor-quiz-answer-run` — four or more questions in a row share an answer
97
+ - `ksor-quiz-contradiction` — an explanation calls the marked answer wrong
98
+ - `ksor-quiz-duplicate-stem` — two questions open with the same 60 characters
99
+
100
+ These are carried from the predecessor, where the same mistakes shipped and were
101
+ found by readers rather than by the project — one quiz put every correct answer
102
+ in the same position across 451 questions. There they lived in a script that was
103
+ run once; here they are part of loading the file, so a quiz that fails them
104
+ cannot be published. The ratio rules do not apply below five questions, where
105
+ enforcing a spread would mean choosing an author's answers for them.
106
+
107
+ `ksor init` ships a quiz on the seed document, so a first `pnpm dev` shows the
108
+ shape. Its own first draft was refused for putting four of five answers at
109
+ option B — the check catching exactly what it was carried for.
110
+
3
111
  ## 0.0.29
4
112
 
5
113
  ### 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-8lNruq9e-CuohjtoK.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-C-OMrGGt.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-DU1nqwZa.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
@@ -4208,6 +4208,14 @@ const ATTACHMENT_SUFFIXES = [
4208
4208
  {
4209
4209
  suffix: ".flashcards.yaml",
4210
4210
  kind: "deck"
4211
+ },
4212
+ {
4213
+ suffix: ".quiz.yaml",
4214
+ kind: "quiz"
4215
+ },
4216
+ {
4217
+ suffix: ".slides.yaml",
4218
+ kind: "slides"
4211
4219
  }
4212
4220
  ];
4213
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-8lNruq9e.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-8lNruq9e-CuohjtoK.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.29",
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"];
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.
@@ -49,6 +55,10 @@ const ATTACHMENT_NEAR_MISSES = [
49
55
  [".flashcards.yml", ".flashcards.yaml"],
50
56
  [".flashcards.json", ".flashcards.yaml"],
51
57
  [".summary.markdown", ".summary.md"],
58
+ [".quiz.yml", ".quiz.yaml"],
59
+ [".quiz.json", ".quiz.yaml"],
60
+ [".slides.yml", ".slides.yaml"],
61
+ [".slides.json", ".slides.yaml"],
52
62
  ];
53
63
 
54
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"];
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.
@@ -49,6 +55,10 @@ const ATTACHMENT_NEAR_MISSES = [
49
55
  [".flashcards.yml", ".flashcards.yaml"],
50
56
  [".flashcards.json", ".flashcards.yaml"],
51
57
  [".summary.markdown", ".summary.md"],
58
+ [".quiz.yml", ".quiz.yaml"],
59
+ [".quiz.json", ".quiz.yaml"],
60
+ [".slides.yml", ".slides.yaml"],
61
+ [".slides.json", ".slides.yaml"],
52
62
  ];
53
63
 
54
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.