@panaversity/ksor 0.0.20 → 0.0.21

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