@panaversity/ksor 0.0.20 → 0.0.22

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 (44) hide show
  1. package/CHANGELOG.md +498 -0
  2. package/dist/cli.mjs +71 -19
  3. package/package.json +3 -3
  4. package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
  5. package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
  6. package/templates/scaffold/AGENTS.md +52 -4
  7. package/templates/scaffold/gitignore +3 -0
  8. package/templates/scaffold/instance.md +28 -20
  9. package/templates/scaffold/knowledge/governance-ladder.md +36 -0
  10. package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
  11. package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
  12. package/templates/scaffold/knowledge/surfaces/index.md +21 -0
  13. package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
  14. package/templates/scaffold/pnpm-lock.yaml +1198 -228
  15. package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
  16. package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
  17. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
  18. package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
  19. package/templates/scaffold/system/site/app/global.css +552 -9
  20. package/templates/scaffold/system/site/app/layout.tsx +23 -4
  21. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
  22. package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
  23. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
  24. package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
  25. package/templates/scaffold/system/site/components/governance.tsx +262 -0
  26. package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
  27. package/templates/scaffold/system/site/components/record-index.tsx +120 -0
  28. package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
  29. package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
  30. package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
  31. package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
  32. package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
  33. package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
  34. package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
  35. package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
  36. package/templates/scaffold/system/site/components.json +25 -0
  37. package/templates/scaffold/system/site/lib/governance.ts +432 -0
  38. package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
  39. package/templates/scaffold/system/site/lib/shared.ts +38 -0
  40. package/templates/scaffold/system/site/lib/source.ts +221 -5
  41. package/templates/scaffold/system/site/lib/stage-knowledge.ts +193 -40
  42. package/templates/scaffold/system/site/lib/utils.ts +6 -0
  43. package/templates/scaffold/system/site/package.json +9 -3
  44. package/templates/scaffold/knowledge/example.md +0 -23
package/CHANGELOG.md CHANGED
@@ -1,5 +1,503 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.22
4
+
5
+ ### Patch Changes
6
+
7
+ - ef538fa: Stage the record under a lock, so a build that evaluates its config more than
8
+ once cannot publish a short site.
9
+
10
+ A site build evaluates `source.config.ts` in more than one process — seven of
11
+ them staged the record in one measured build — and staging was destructive on
12
+ every evaluation: delete the whole per-audience stage, refill it. Two of those
13
+ overlapping deleted a tree the other was copying into. Six
14
+ concurrent evaluations of a 150-document record failed 42 of 48 runs — `ENOENT`
15
+ and `EINVAL` out of `copyFileSync`, `ENOTEMPTY` out of `rmSync` despite its
16
+ retries, and, in 27 of the 48, no error at all: staging returned success and
17
+ handed the build a stage a third of the record short. That last shape is the one
18
+ that matters — a crash fails a build, a short stage publishes one, with
19
+ documents missing from `/docs`, `llms.txt` and the search index and nothing
20
+ saying so.
21
+
22
+ Staging now takes a lock file (`system/site/.staged-knowledge.lock`, gitignored,
23
+ stamped with the holder's pid so a killed build's lock is broken rather than
24
+ waited on), and an evaluation that finds the stage already holding exactly its
25
+ plan — byte for byte — leaves it alone instead of rebuilding it. Together those
26
+ mean the destructive path runs once per build, alone. No behaviour changes for a
27
+ build that was already succeeding.
28
+
29
+ ## 0.0.21
30
+
31
+ ### Patch Changes
32
+
33
+ - 2c67e18: The files AI agents read now carry the same governance the page shows.
34
+
35
+ A scaffolded site warned a reader that a policy had been replaced — an
36
+ unmissable notice above the title, naming its successor — and then handed an
37
+ agent the same policy as ordinary prose. In `llms.txt` a withdrawn document and
38
+ the one that replaced it were adjacent entries, told apart only by whatever a
39
+ human happened to type into a title; in `llms-full.txt` the withdrawn body
40
+ appeared with no status, no successor and no owner at all. An agent reading the
41
+ record answered from a policy nobody follows any more, and had nothing in the
42
+ bytes it was given to know that.
43
+
44
+ `llms.txt` now marks a document whose status is a caveat — `DRAFT`, `REVIEW`,
45
+ `SUPERSEDED` — and names the route that replaced a superseded one.
46
+ `llms-full.txt` puts the record's own keys back as frontmatter above each
47
+ document: status, owner, effective, the resolved successor, and every
48
+ provenance entry.
49
+
50
+ Two details are deliberate. A successor is named by the route a consumer can
51
+ fetch, never the `./successor.md` pointer it has no file tree to resolve. And
52
+ `site: governance: false` does not reach these files — that key decides what the
53
+ published pages show, while the record keeps every key for the agent surface and
54
+ the audit trail, so suppressing them here would recreate the defect on purpose.
55
+
56
+ Nothing else changes: no new dependency, no new frontmatter key, the same static
57
+ export. An approved document's index line stays clean, and a document that
58
+ declares no governance still emits none — a placeholder would read as governed.
59
+
60
+ - 472ee30: An interrupted ingest no longer throws away the embeddings it already paid for
61
+
62
+ A killed `ksor ingest` leaves its generation in state `building`. Carry-forward
63
+ accepted only `ready`, `active` and `retired` sources, so the rerun found nothing
64
+ to copy and embedded the entire corpus again — paying twice for work that was
65
+ sitting in the database, correct and complete.
66
+
67
+ Found while ingesting an 81-document book into a managed Postgres. The run was
68
+ killed at 4,736 of 6,963 chunks; the rerun reported `carried 0, pending 6963`.
69
+
70
+ ```
71
+ before structure: 82 nodes, 81 sources, 6963 chunks; carried 0, pending 6963
72
+ after structure: 82 nodes, 81 sources, 6963 chunks; carried 4736, pending 2227
73
+ ```
74
+
75
+ The asymmetry is what made it expensive. Interrupt a RE-ingest and a complete
76
+ generation still exists, so the rerun carries from it and costs almost nothing.
77
+ Interrupt the FIRST ingest and there is no complete generation at all — and the
78
+ first ingest of a large corpus is the longest, the least familiar, and the one an
79
+ operator is most likely to interrupt.
80
+
81
+ Nothing about an abandoned run makes its vectors wrong. An embedding is a pure
82
+ function of its input and model, the match key already establishes identity, and
83
+ carry only ever fills chunks still marked pending. So a run's state now decides
84
+ the ORDER sources are tried in, not whether they may be used at all: the active
85
+ generation first, so vetted vectors always win, then complete generations newest
86
+ first, then abandoned ones.
87
+
88
+ - 59c4f7a: **The search dialog forgets the last search when you close it.** The query
89
+ lives in component state and the dialog stays mounted after closing, so the
90
+ next time a reader opened search it came up on the previous term and its
91
+ results — they had to clear the field before they could look for anything
92
+ else. Closing now resets it, and the shell's own `onOpenChange` still fires,
93
+ so nothing else about the dialog changes.
94
+ - 2c67e18: **The scaffolded site moves to Fumadocs 16.14.5 / fumadocs-mdx 15.3.0**, from
95
+ 16.10.3 / 15.0.13.
96
+
97
+ What the adopter gets, all of it landing at or below 16.14.5:
98
+
99
+ - **Search is multilingual with no configuration.** 16.14.0 replaced the Orama
100
+ engine with ZBSearch behind the same module paths. The scaffold now imports
101
+ `staticClient` rather than the deprecated `oramaStaticClient` alias it kept
102
+ for compatibility — the subpath and the options are unchanged, so the new
103
+ name costs nothing today and does not have to be found again when the alias
104
+ goes. It matters here because a KSoR's knowledge is written in whatever
105
+ language its owner writes in.
106
+ - **Two accessibility fixes**: the sidebar trigger exposes its state to
107
+ assistive technology (16.11.5), and documentation pages carry a `main`
108
+ landmark (16.14.5).
109
+ - **A table-of-contents overscroll fix** (16.14.3), which this shell feels
110
+ because it holds the TOC column on every page.
111
+ - **Page Actions honour a base path** (16.10.7) — relevant because the scaffold
112
+ ships `KSOR_BASE_PATH` for sub-path hosting.
113
+
114
+ **Not 16.15.0 / 15.3.1, deliberately.** Those are the `latest` tags, but they
115
+ were published 2026-08-21 18:05Z and the scaffold's own supply-chain policy
116
+ quarantines a dependency for 48 hours (`minimumReleaseAge: 2880`). Pinning them
117
+ today would emit a scaffold whose first `pnpm install` its own policy refuses.
118
+ Every improvement listed above is at or below 16.14.5, so nothing is given up
119
+ by waiting; the bump is a one-line change once they age out.
120
+
121
+ Also worth recording: `fumadocs-core` and `fumadocs-ui` both have a `17.0.0` on
122
+ npm, published 2026-02-01 — BEFORE the 16.x line. The `latest` tag is 16.x. A
123
+ higher version number is not a later release here, and nothing should chase it.
124
+
125
+ **The sidebar's status marker is the shell's plugin now, not our own walk.**
126
+ `statusBadgesPlugin` reads `status` from a document's frontmatter while the
127
+ loader builds the page tree, so the scaffold stops carrying a map of statuses
128
+ by url and a second recursive walk that rewrote each row. What stays ours is
129
+ the rule the shell has no opinion about: only a CAVEAT is drawn, so `approved`
130
+ renders nothing and the marker stays rare enough to be noticed. The tree nodes
131
+ also gain a real `status` field rather than only a decorated name.
132
+
133
+ **Every document can be handed to an agent in one click.** Beside the link to a
134
+ document's markdown twin there is now a `Copy` action that fetches that same
135
+ twin — the bytes `/md/<path>.md` already serves, so there is no second
136
+ rendering of the document to drift — and puts them on the clipboard. Opening
137
+ the markdown and handing it to an agent are different acts, and a reader who
138
+ wants the second should not have to perform the first.
139
+
140
+ Fumadocs ships an `ai/page-actions` component that does this alongside "Open in
141
+ ChatGPT" and "Open in Claude". Those two are deliberately not taken: this
142
+ product's claim is that one corpus answers in ANY assistant because the agent
143
+ surface is an open standard, and hardcoding two vendors into every adopter's
144
+ page argues the opposite. What is taken is the shell's own `useCopyButton`
145
+ hook, which owns the copied-state timing — the only part worth not rewriting —
146
+ so the action costs no new dependency and no registry component.
147
+
148
+ It fails honestly: `navigator.clipboard` exists only in a secure context, so a
149
+ site served over plain http on a LAN address has no clipboard at all, and the
150
+ button says "Copy failed" rather than reporting a success it did not have.
151
+
152
+ **The table of contents marks where you are, not everything in view.** Fumadocs
153
+ defaults its TOC to `single: false`, which marks EVERY heading currently on
154
+ screen as active — and a governed record is full of short documents whose
155
+ headings all fit on one screen, so the whole rail rendered in the accent at
156
+ once (measured: four of five entries active on a five-heading document). An
157
+ accent that marks everything marks nothing. The default is specifically wrong
158
+ for this shape of content, so the scaffold sets `single: true`.
159
+
160
+ The two document actions rest in muted grey with an icon each, and take the
161
+ accent only when something has happened — the copy that succeeded. They wore
162
+ the accent at rest, which said "link" about controls that were merely sitting
163
+ there and added to a page already too blue to read.
164
+
165
+ **"On this page" marks the section you are in, exactly.** The shell decides the
166
+ active heading with an intersection observer set to `{ threshold: 0.9 }` and no
167
+ `rootMargin` — a heading counts as active whenever 90% of it is visible
168
+ ANYWHERE in the viewport — and then highlights whichever became active most
169
+ recently. On a long page that reads fine. On a governed record it does not:
170
+ these documents are short-sectioned, so several headings share the screen and
171
+ the one arriving from the BOTTOM always won. The marker sat two to four
172
+ headings ahead of the reader (measured: reading "owner" while the rail marked
173
+ "description").
174
+
175
+ Those observer options are not configurable and the observer is not exported,
176
+ so the selection could not be corrected — only replaced. The scaffold now
177
+ supplies the rail through `DocsPage`'s `slots.toc.main`, keeping the shell's
178
+ provider and its small-screen popover exactly as they are. The rule is a
179
+ reading line rather than visibility: the active heading is the last one whose
180
+ top has passed it, which is what a person means by "the section I am in".
181
+ Measured at eight scroll positions across a 7.8-screen document: exact at every
182
+ one. The bar is the row's own border, so it cannot drift from what it marks.
183
+
184
+ - 1145ebb: When calibration does not separate, `ksor calibrate` names the probes that held it open
185
+
186
+ A "NOT separable" verdict reads as _this corpus cannot be calibrated_, and the
187
+ report had every number needed to show otherwise while printing none of them.
188
+ Its only remedy was "widen the probe set" — when the fix is sometimes to narrow
189
+ it.
190
+
191
+ ```
192
+ these out-of-corpus probes scored at or above your weakest in-corpus question:
193
+ 0.721 which vector database should I choose
194
+ ^ look at these first. Either the record COVERS one — move it to the
195
+ in-corpus side, because a probe the record answers is not out of corpus
196
+ — or it genuinely does not separate, and the floor stays uncalibrated.
197
+ ```
198
+
199
+ That is a real measurement, on a real 81-document book. One probe — a question
200
+ about vector databases, asked of a record containing a Postgres-and-AI chapter —
201
+ held the whole calibration open at 0.721 against a weakest in-corpus question of
202
+ 0.680. It was not an out-of-corpus question at all; it was mislabelled. Moving it
203
+ separated the record immediately (`max OOC 0.676 < min in-corpus 0.680`), and the
204
+ resulting floor answered every in-corpus question and refused every genuine
205
+ out-of-corpus one.
206
+
207
+ Without that line, the conclusion drawn from the same numbers was that the record
208
+ could not support abstention — the product's headline guarantee — at all.
209
+
210
+ The advice deliberately names **both** readings, because either can be right: the
211
+ probe may be mislabelled, or the corpus may genuinely fail to separate, in which
212
+ case the floor stays uncalibrated and that is the correct outcome.
213
+
214
+ - 2c67e18: The scaffolded site now renders the governance each document declares.
215
+
216
+ `knowledge/` documents carry `status`, `owner`, `provenance`, `effective` and
217
+ `superseded_by`, and `pnpm check` enforces them — but the site rendered only
218
+ title, description and body. The sharpest consequence was not cosmetic: a
219
+ `status: superseded` document was served looking identical to an approved one,
220
+ with the successor pointer the checker requires swallowed.
221
+
222
+ Each document now shows its owner and effective date under the title, one entry
223
+ per `provenance` source at the foot, and — above the title, where it cannot be
224
+ missed — a supersession notice that names the successor and links to its page.
225
+
226
+ The status appears only when it is a caveat: `draft`, `review` and `superseded`
227
+ are shown, `approved` is not. A reader already assumes a document in a system of
228
+ record is current, and a label that appears on every page saying the same thing
229
+ trains people to skip it — including on the page where it mattered.
230
+
231
+ Nothing is inferred. A key a document does not declare renders nothing at all:
232
+ a placeholder would read as governed, which is worse than a visible gap. It is
233
+ all server-rendered, so the governance survives printing, JavaScript off and a
234
+ failed bundle.
235
+
236
+ Whether the pages show it at all is the owner's call: `site: governance: false`
237
+ in `instance.md` keeps them plain while the record still carries every key for
238
+ the agent surface and the audit trail. It defaults to on, and it never hides
239
+ the supersession notice — a reader handed a replaced document with no word of
240
+ its successor has been misled regardless of the site's preferences.
241
+
242
+ The record's checker was hardened alongside, because these keys now reach a
243
+ published page: `superseded_by` is validated whatever shape it is written in
244
+ (a pointer matching neither `./x` nor `*.md` previously skipped every rule,
245
+ including the cross-audience one, and the page then published it verbatim); it
246
+ must name a real markdown document, not a directory, and must pair with
247
+ `status: superseded`; an `effective` carrying a time is refused, because a YAML
248
+ timestamp reads back in a timezone and could render the day before the one
249
+ written; and a grouped `instance.md` key written inline (`site: { … }`) is
250
+ refused instead of being silently dropped, which also restores the closed-key-set
251
+ guarantee for every nested group.
252
+
253
+ A second adversarial pass hardened the rules again: the `effective` check now
254
+ matches YAML's real timestamp grammar rather than a padded-date shape (so
255
+ `2026-4-1 00:00:00 +05:00` is caught and `2026-04-01 for new customers` is left
256
+ alone); a YAML comment on an `instance.md` group key and a capitalised `False`
257
+ are accepted, both having been refused by a checker stricter than the parsers it
258
+ protects; a supersession that points back at itself or forms a cycle is refused,
259
+ because the notice was sending readers in a circle; and a long source URL now
260
+ wraps instead of being clipped away on a phone.
261
+
262
+ - 2c67e18: The scaffolded site got a UI pass, driven by measuring the real page in a
263
+ browser rather than reading the code.
264
+
265
+ **Every document is now published as markdown too.** `/md/<path>.md` carries the
266
+ document's body and its governance as frontmatter, and each page advertises its
267
+ twin with a `rel="alternate"` link and a visible "This document as markdown"
268
+ line. An agent handed a document URL no longer has to scrape a React app to
269
+ reach text the record holds verbatim.
270
+
271
+ **Governance shows up where a reader chooses, not only after the click.** The
272
+ sidebar, the previous/next pager, search results, the home page and every folder
273
+ index now carry a caveat status, so a withdrawn document and the one that replaced it stop
274
+ looking identical at the moment you pick between them.
275
+
276
+ **A folder page lists what the folder holds**, and the home page lists the
277
+ record — it used to announce a document count and link to one of them.
278
+
279
+ **The home page opens with the record's own words**: the first paragraph of
280
+ `instance.md`, which is also what `ksor serve` gives the MCP server. The
281
+ framework's marketing line is gone from the adopter's front page, which the
282
+ project's own critical rule 1 never allowed. Scaffolded `instance.md` was
283
+ reordered so the authority sentence comes first, where it belongs for the system
284
+ prompt too.
285
+
286
+ **Supersession runs both ways.** The withdrawn document names its successor; the
287
+ successor now names what it replaced, derived from the record with no new
288
+ frontmatter key.
289
+
290
+ **The supersession notice reads as a caution and is reachable by landmark** —
291
+ its own colour instead of the brand accent that also means "go here", and
292
+ `role="region"` with `aria-labelledby` instead of `role="note"`.
293
+
294
+ **A provenance entry that is a URL is now a link** — the whole entry only, and
295
+ `http(s)` only, so an authored `javascript:` source can never become a
296
+ clickable href.
297
+
298
+ **The sidebar footer no longer renders an empty input-shaped box.** The theme
299
+ switch shipped inside a bordered bar that stretched to the sidebar width around
300
+ one 61px control; it now sits on the footer row beside the mark.
301
+
302
+ **The left rail is flush with the window again.** The docs grid gives the
303
+ sidebar panel the centring offset as well as its own column, so above 97rem the
304
+ panel's surface ran to the window edge with the first nav item starting 103px
305
+ inside it (measured at 1728px). The layout width is now `100%`: the offsets go
306
+ to zero, the rail starts where the window does, and the prose does not move.
307
+
308
+ **The site is a shadcn/ui project.** `components.json` and `lib/utils.ts` ship
309
+ with the scaffold, so `pnpm dlx shadcn@latest add <name>` writes a component the
310
+ adopter then owns, and Fumadocs reads the same palette through its `shadcn` CSS
311
+ preset — one set of tokens for the shell and for anything added from the
312
+ registry, with `--primary` carrying the brand. It also ends a real defect: the
313
+ `neutral` preset painted the page and the sidebar it sits against 1.6% apart, so
314
+ the reading surface never read as a page. The shadcn CLI itself is deliberately
315
+ NOT a dependency (578 extra packages, measured); the four the site actually uses
316
+ cost +2.
317
+
318
+ **The previous/next neighbours sit at the foot of the page, not wherever the
319
+ text stopped.** A governed record is full of short documents, and on those the
320
+ pager landed mid-screen — 265px above the bottom edge on the policies index,
321
+ measured — reading as more content rather than as the end of the page. It now
322
+ takes the free space as margin above it, and stays exactly where it was on any
323
+ document taller than the viewport.
324
+
325
+ **The reading column stopped moving, and stopped being a slab.** The shell caps
326
+ the article at 900px — 78 characters a line at the body's 16px — and centres it
327
+ in whatever the table-of-contents column leaves, so the prose ALSO jumped 134px
328
+ sideways between a document with headings and one without (measured: text at
329
+ x=446 against x=580). The measure is now 46rem, about 66 characters, and the
330
+ TOC column is held on every page, so sidebar and rail are the same width and the
331
+ column lands in the same place on every document: x=464, 672px wide, on a
332
+ document with a table of contents and on one without, verified in both.
333
+
334
+ **The home page is the record's own front door.** It is a landing page that
335
+ stands alone — no sidebar, no document chrome, `Open the record` as the way in,
336
+ landing on the first document in governed order rather than a hardcoded path.
337
+ Every word on it comes from `instance.md` or a document's frontmatter, because
338
+ the site contains no authored content, and everything it says is in the
339
+ server-rendered markup, so a crawler, a reader without JavaScript and an agent
340
+ parsing the HTML all read the same page.
341
+
342
+ **The site has a design, not a default theme.** Three voices, each marking who
343
+ is speaking: the record's own words in a serif (its title, its documents'
344
+ titles), the site's furniture in a sans, and everything machine-facing — the
345
+ slug, addresses, owners, statuses, section labels — in mono. System stacks only,
346
+ because a web font is fetched at build time and the scaffold's build must work
347
+ offline and byte-identically. The palette moves from neutral grey to a cool ink
348
+ (`oklch(0.17 0.012 255)`) that sits with the accent instead of beside it, with
349
+ firmer hairlines, and the accent is spent only on actions, links and the active
350
+ state.
351
+
352
+ **The front door is the record's cover, and it is one screen.** The identity
353
+ takes the whole window under the navbar over a faintly ruled ground: the
354
+ record's name, the authority sentence it declares in `instance.md`, one way in,
355
+ and the record itself standing beside it. The cover follows the theme
356
+ rather than staying dark in both — pale stock in the light, and in the dark it
357
+ rises one step above the page instead of turning white, because a cover is the
358
+ surface that catches the light. Every machine address came off the page:
359
+ `/llms.txt` sits where agents look for it and each document advertises its own
360
+ markdown twin, so nothing became less discoverable by leaving the front door,
361
+ and the page stopped printing URLs at a reader who will never fetch one.
362
+
363
+ **The front door shows the record, not a drawing of one.** The right of the
364
+ cover is the document `Open the record` opens — its own title, its own words,
365
+ its owner and any caveat status — with the record's next entries standing
366
+ behind it, and the count of everything the record holds beneath. Four abstract
367
+ illustrations were drawn for that space first and all four were rejected; the
368
+ reason is the useful part, and it is now written into the component: a stock
369
+ drawing is the ONE thing on this page that can never be true of the adopter's
370
+ corpus, so every KSoR would have shipped the same picture of nothing in
371
+ particular. A record of one document and a record of two hundred now get
372
+ visibly different front doors. Nothing on the page is authored — every string
373
+ is a title, description, owner or status the record itself declares — and the
374
+ depth is CSS, so it costs no image, needs no request, and follows the theme.
375
+
376
+ **The cover's composition is centred and its type ramp closed.** The signature
377
+ line at the foot took the section's free space as top margin, which cancelled
378
+ the centring and left 197px of dead space below the content and none above it
379
+ (measured at a 996px-tall window); the composition now sits in the middle of the
380
+ space above the signature, 157px clear at the top and 158px at the bottom. The
381
+ ramp ran 12px eyebrow to a 76px title to an 18px lead — a jump with nothing in
382
+ the middle — and is now 12 / 64 / 20. The accent rule under the title was still
383
+ pinned to the dark theme's blue from when the cover was dark in both themes,
384
+ which left it all but invisible on the pale light cover; it takes the token
385
+ again, so it inverts with everything else.
386
+
387
+ **A document's section headings speak in the record's voice, and its
388
+ governance strip has a hierarchy.** Only the title was styled; h2, h3 and h4
389
+ fell through to the shell's prose defaults, which measured 24 / 20 / 16px in
390
+ the SANS body face — so inside one document the title was the record speaking
391
+ and every section heading was the site speaking, and h4 was the body size
392
+ with only its weight to tell it apart. The ramp is now 38 / 28 / 22 / 18 in
393
+ the display serif, scoped to the container the record's own markdown renders
394
+ in so the site's own headings keep their voice.
395
+
396
+ In the strip under the title, the label and its value were both mono a single
397
+ pixel apart, so "Owner Product Effective 2026-08-22" read as one
398
+ undifferentiated run. The label is now 10px and letterspaced against a 13px
399
+ value that carries the weight. The "Markdown" link stopped wearing the
400
+ bordered badge that means "a status the record declares": it is the one
401
+ ACTION on a row of FACTS, so it takes the accent, which on this site means a
402
+ link. The gap between facts matches the register's.
403
+
404
+ **A withdrawn document no longer looks like a draft.** `draft` and `superseded`
405
+ rendered as pixel-identical chips — same hairline border, same muted text —
406
+ which put the two statuses that mean the most different things in the same
407
+ clothes at exactly the moment a reader picks between a document and its
408
+ successor. `--ksor-caution` already existed to mean "the record withdrew this",
409
+ but it was declared inside the one class that first used it; it is now a token
410
+ pair on the root, and a `superseded` chip wears it in all five places one
411
+ renders: the sidebar, the section listing, the front door's stack, the
412
+ document's own governance strip, and search results. The colour is additive and
413
+ never the whole signal — the word "superseded" is beside it everywhere.
414
+
415
+ **The ramp covers every level a document can write, and the top of a document
416
+ stopped moving.** Three defects an audit of the shipped stylesheet turned up
417
+ after the first pass: a body `# heading` was reached by neither rule — the page
418
+ title's selector is a child combinator — so it rendered at 30px in the SANS
419
+ face at weight 800, the loudest thing on the page, in the site's voice, for the
420
+ record's own words; `h5` and `h6` were 16px/400 with no margins at all, which
421
+ is a paragraph, because the prose plugin never defines them and preflight
422
+ resets them; and the ramp's own margins beat the shell's "first block has no
423
+ top margin" rule, which is written with `:where()` and therefore has no
424
+ specificity, so a document whose body opened with a heading started 44px lower
425
+ than one that opened with a sentence. All three fixed and measured.
426
+
427
+ **A document reads like documentation, not a wall of black.** Under the
428
+ headings almost everything was one weight of one ink: a link inside a paragraph
429
+ rendered at the same colour and weight as the `<strong>` beside it, told apart
430
+ only by an underline, so nothing on the page looked clickable. Links in running
431
+ text now take the accent — in running text only, because a heading carries an
432
+ anchor around its own words and colouring those turns every heading blue.
433
+ Emphasis is heavier than the 500 the prose default gave it. A table's head
434
+ speaks in the mono voice every other label on the site uses and its rows are
435
+ separated by hairlines, where `tbody tr` previously had a 0px border and the
436
+ cells simply floated. A quotation steps back in muted ink instead of shouting
437
+ in italic. And a fenced code block finally looks like a block: Fumadocs paints
438
+ its surface with `bg-fd-card`, which in this palette is under 2% away from the
439
+ page colour, so it takes `--muted` — the token that actually means "a surface
440
+ on the page", and a light/dark pair.
441
+
442
+ **Inline code carries its own colour.** A frontmatter key or a path in running
443
+ text was set in the same ink as the prose, leaving a grey chip to do the whole
444
+ job of saying "this is machine vocabulary". It now has a token of its own — a
445
+ deep teal, as a light/dark pair — chosen because the two colours already on
446
+ this site are spoken for: the accent means link or action, so tinting code with
447
+ it would make every key look clickable, and `--ksor-caution` means the record
448
+ withdrew something.
449
+
450
+ **A record's entries look clickable before you touch them.** The list a folder
451
+ page shows was a hairline register whose rows were links and said so only on
452
+ hover — so on a touch screen, where there is no hover, nothing ever indicated
453
+ they could be opened. Each entry is a card now: a bordered surface, an icon
454
+ that distinguishes a folder from a document, the title in the record's serif,
455
+ its metadata in mono, and an arrow that says where pressing leads. The voice is
456
+ unchanged, and so is the rule that `approved` shows no label.
457
+
458
+ - 2c67e18: **`ksor init` seeds a real starter record instead of one bare stub.**
459
+
460
+ A fresh project used to arrive with a single document titled "Your first
461
+ governed document" — enough to prove the directory was not empty, and nothing
462
+ more. The first `pnpm dev` therefore showed a site with one page on it, which is
463
+ the worst possible demonstration of a system whose whole subject is a governed
464
+ body of knowledge: no folder, no owner, no provenance, no supersession, no
465
+ second status, and a front door with one card on it.
466
+
467
+ Five documents now ship in `knowledge/`, in a two-level shape:
468
+
469
+ ```text
470
+ knowledge/
471
+ ├── what-is-a-ksor.md
472
+ ├── governance-ladder.md
473
+ └── surfaces/
474
+ ├── index.md
475
+ ├── for-people.md
476
+ └── for-agents.md
477
+ ```
478
+
479
+ They are about KSoR itself, and they carry the governance keys they describe —
480
+ owners, `provenance` naming real sources, effective dates, `order`, and one
481
+ `draft` beside four `approved`. So the governance surfaces are visible working
482
+ on the first run: a caveat status in the sidebar and on the front door, a
483
+ folder that counts what it holds, provenance rendered at the foot of a page,
484
+ and `llms.txt` carrying the same facts to an agent.
485
+
486
+ `instance.md` leads with the matching authority sentence and its display title
487
+ is `KSoR`, so the site has a coherent identity out of the box rather than a
488
+ placeholder. Both the record and the identity are seed content the adopter
489
+ replaces: `instance.md` says so in its own body, and the intake interview
490
+ rewrites the identity as its first job.
491
+
492
+ The record is still the adopter's outright (decision 4) — these are documents
493
+ to delete as real knowledge arrives, not framework files to work around.
494
+
495
+ The seeded documents are sectioned rather than flat — `##` down to `####`,
496
+ never an `# h1`, because the frontmatter title is already the page heading.
497
+ A record of headingless documents would leave both the document heading ramp
498
+ and the "On this page" table of contents unexercised on exactly the pages an
499
+ adopter reads first.
500
+
3
501
  ## 0.0.20
4
502
 
5
503
  ### Patch Changes
package/dist/cli.mjs CHANGED
@@ -15,7 +15,7 @@ import { bodyLimit } from "hono/body-limit";
15
15
  import { execFileSync, spawnSync } from "node:child_process";
16
16
  import { parseArgs } from "node:util";
17
17
  import { readFile, readdir, stat } from "node:fs/promises";
18
- //#region ../content-gateway/dist/main-9UsxyoZa.mjs
18
+ //#region ../content-gateway/dist/main-DKA8sPwv.mjs
19
19
  /**
20
20
  * A connection could not be ESTABLISHED in time — retryable.
21
21
  *
@@ -4574,7 +4574,7 @@ async function withPgRetry(op, options = {}) {
4574
4574
  throw lastError;
4575
4575
  }
4576
4576
  //#endregion
4577
- //#region ../content/dist/commands-DcPJJlNb.mjs
4577
+ //#region ../content/dist/commands-FLQD4HUi.mjs
4578
4578
  /**
4579
4579
  * EVAL-LOCKED constants, quarried verbatim from the oracle
4580
4580
  * (sor-agentfactory @ b554f91, config.py) — changing any of these is a
@@ -6590,19 +6590,42 @@ async function allocateRun(client, opts) {
6590
6590
  *
6591
6591
  * Returns 0 when there is no complete embedded generation — the first ingest.
6592
6592
  */
6593
- async function bestCarrySource(client, opts) {
6594
- const gen = (await client.query(`
6595
- SELECT max(c.generation) AS gen FROM chunks c
6596
- JOIN ingestion_runs r ON r.tenant_id = c.tenant_id AND r.generation = c.generation
6597
- WHERE c.tenant_id = $1 AND r.corpus_id = $2
6598
- AND r.state IN ('ready', 'active', 'retired')
6599
- AND c.generation <> $3 AND c.embedding_status = 'embedded'
6593
+ /**
6594
+ * Every generation whose vectors this build may copy, best source FIRST.
6595
+ *
6596
+ * Ordered by how much the source has been vetted, then by recency:
6597
+ *
6598
+ * 1. complete runs (`ready` / `active` / `retired`), newest first
6599
+ * 2. ABANDONED runs (`building`), newest first
6600
+ *
6601
+ * The second group used to be excluded outright, and that threw away work an
6602
+ * operator had already paid for: a killed `ksor ingest` leaves its generation in
6603
+ * `building`, so the rerun carried NOTHING and re-embedded the whole corpus.
6604
+ * Reproduced live — an 81-document book killed at 4,736 of 6,963 chunks, whose
6605
+ * rerun reported `carried 0, pending 6963` (issue #97).
6606
+ *
6607
+ * Nothing about an abandoned run makes its vectors wrong. An embedding is a pure
6608
+ * function of (embed input, model); the match key in `carryForward` establishes
6609
+ * identity on its own; and carry only ever fills `pending` rows, so a later pass
6610
+ * can never overwrite an earlier, better-vetted one. The run's STATE therefore
6611
+ * decides priority, not eligibility — which is what ordering expresses and
6612
+ * exclusion could not.
6613
+ */
6614
+ async function carrySources(client, opts) {
6615
+ return (await client.query(`
6616
+ SELECT DISTINCT c.generation AS gen,
6617
+ CASE WHEN r.state IN ('ready','active','retired') THEN 0 ELSE 1 END AS rank
6618
+ FROM chunks c
6619
+ JOIN ingestion_runs r ON r.tenant_id = c.tenant_id AND r.generation = c.generation
6620
+ WHERE c.tenant_id = $1 AND r.corpus_id = $2
6621
+ AND r.state IN ('ready', 'active', 'retired', 'building')
6622
+ AND c.generation <> $3 AND c.embedding_status = 'embedded'
6623
+ ORDER BY rank, gen DESC
6600
6624
  `, [
6601
6625
  opts.tenantId,
6602
6626
  opts.corpusId,
6603
6627
  opts.excludeGeneration
6604
- ])).rows[0]?.gen ?? null;
6605
- return gen === null ? 0 : Number(gen);
6628
+ ])).rows.map((r) => Number(r.gen));
6606
6629
  }
6607
6630
  /**
6608
6631
  * Copy embeddings for chunks whose ENTIRE embed input is unchanged (hash +
@@ -8558,17 +8581,19 @@ async function buildStructure(client, opts) {
8558
8581
  fromGeneration: active,
8559
8582
  modelId
8560
8583
  });
8561
- const newest = await bestCarrySource(client, {
8584
+ for (const source of await carrySources(client, {
8562
8585
  tenantId,
8563
8586
  corpusId: opts.corpusId,
8564
8587
  excludeGeneration: generation
8565
- });
8566
- if (newest !== 0 && newest !== active) carried += await carryForward(client, {
8567
- tenantId,
8568
- generation,
8569
- fromGeneration: newest,
8570
- modelId
8571
- });
8588
+ })) {
8589
+ if (source === active) continue;
8590
+ carried += await carryForward(client, {
8591
+ tenantId,
8592
+ generation,
8593
+ fromGeneration: source,
8594
+ modelId
8595
+ });
8596
+ }
8572
8597
  const health = await generationHealth(client, {
8573
8598
  tenantId,
8574
8599
  generation
@@ -8880,6 +8905,31 @@ async function buildGeneration(pool, instance, options) {
8880
8905
  function pct(fraction) {
8881
8906
  return `${(fraction * 100).toFixed(0)}%`;
8882
8907
  }
8908
+ /**
8909
+ * Out-of-corpus probes scoring at or above the weakest in-corpus question,
8910
+ * worst first — the ones that decided the verdict. Empty when the measurement
8911
+ * separated, because then nothing held it open.
8912
+ */
8913
+ function overlappingProbes(report) {
8914
+ if (report.separable) return [];
8915
+ const weakest = report.low_tail[0]?.score;
8916
+ if (weakest === void 0) return [];
8917
+ return report.detail.filter((d) => !d.in_corpus && d.score >= weakest).toSorted((a, b) => b.score - a.score);
8918
+ }
8919
+ /**
8920
+ * The guidance itself, or null when there is nothing to say.
8921
+ *
8922
+ * Deliberately names BOTH readings. The overlapping probe is sometimes a
8923
+ * question the record covers — mislabelled, and the measurement is fine once it
8924
+ * moves — and sometimes a genuine near-miss the corpus simply cannot separate,
8925
+ * in which case the floor stays uncalibrated and that is the correct outcome.
8926
+ * Asserting either one alone would send half the readers the wrong way.
8927
+ */
8928
+ function overlapAdvice(report) {
8929
+ const overlapping = overlappingProbes(report);
8930
+ if (overlapping.length === 0) return null;
8931
+ return "these out-of-corpus probes scored at or above your weakest in-corpus question:\n" + overlapping.map((d) => ` ${d.score.toFixed(3)} ${d.query}\n`).join("") + " ^ look at these first. Either the record COVERS one — move it to the\n in-corpus side, because a probe the record answers is not out of corpus\n — or it genuinely does not separate, and the floor stays uncalibrated.\n";
8932
+ }
8883
8933
  /** One transaction (tenant GUC + ingest role): list collectables, then reap each. */
8884
8934
  async function runGc(pool, instance, options = {}) {
8885
8935
  const dryRun = options.dryRun === true;
@@ -9357,6 +9407,8 @@ async function calibrateCommand(args) {
9357
9407
  minChars: values["min-chars"] === void 0 ? void 0 : intFlag("--min-chars", values["min-chars"])
9358
9408
  }));
9359
9409
  process.stdout.write(renderReport(report) + "\n");
9410
+ const advice = overlapAdvice(report);
9411
+ if (advice !== null) process.stdout.write(advice);
9360
9412
  return 0;
9361
9413
  }
9362
9414
  async function grantCommand(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
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",
@@ -66,8 +66,8 @@
66
66
  "tsdown": "0.22.14",
67
67
  "typescript": "7.0.2",
68
68
  "vitest": "^4.1.10",
69
- "@panaversity/ksor-content-gateway": "0.0.0",
70
- "@panaversity/ksor-content": "0.0.0"
69
+ "@panaversity/ksor-content": "0.0.0",
70
+ "@panaversity/ksor-content-gateway": "0.0.0"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=24"