@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.
- package/CHANGELOG.md +498 -0
- package/dist/cli.mjs +71 -19
- package/package.json +3 -3
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
- package/templates/scaffold/AGENTS.md +52 -4
- package/templates/scaffold/gitignore +3 -0
- package/templates/scaffold/instance.md +28 -20
- package/templates/scaffold/knowledge/governance-ladder.md +36 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
- package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
- package/templates/scaffold/knowledge/surfaces/index.md +21 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
- package/templates/scaffold/pnpm-lock.yaml +1198 -228
- package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
- package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
- package/templates/scaffold/system/site/app/global.css +552 -9
- package/templates/scaffold/system/site/app/layout.tsx +23 -4
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
- package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
- package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
- package/templates/scaffold/system/site/components/governance.tsx +262 -0
- package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
- package/templates/scaffold/system/site/components/record-index.tsx +120 -0
- package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
- package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
- package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
- package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
- package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
- package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
- package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
- package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
- package/templates/scaffold/system/site/components.json +25 -0
- package/templates/scaffold/system/site/lib/governance.ts +432 -0
- package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
- package/templates/scaffold/system/site/lib/shared.ts +38 -0
- package/templates/scaffold/system/site/lib/source.ts +221 -5
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +193 -40
- package/templates/scaffold/system/site/lib/utils.ts +6 -0
- package/templates/scaffold/system/site/package.json +9 -3
- 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-
|
|
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-
|
|
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
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
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
|
|
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
|
|
8584
|
+
for (const source of await carrySources(client, {
|
|
8562
8585
|
tenantId,
|
|
8563
8586
|
corpusId: opts.corpusId,
|
|
8564
8587
|
excludeGeneration: generation
|
|
8565
|
-
})
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
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.
|
|
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
|
|
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"
|