@panaversity/ksor 0.0.53 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,333 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.55
4
+
5
+ ### Patch Changes
6
+
7
+ - 4ecf549: Test infrastructure only — nothing an adopter installs behaves differently.
8
+
9
+ The behavioural evals scored a missing `top_cosine` as `-1`. When a provider
10
+ rate-limits, the read plane degrades to keyword-only by design, so searches
11
+ answer with no score — and the assertions then compared sentinels, reporting a
12
+ vendor outage as "the abstention floor is broken". Four CI failures in a day
13
+ read that way before anyone looked past the assertion. A missing score now
14
+ refuses, naming the cause, and never invents the number that is absent.
15
+
16
+ - e476197: Fix the hello-world tutorial, which could not be completed as written.
17
+
18
+ Three defects, all found by walking the published 0.0.54 rather than by reading:
19
+
20
+ - Step 3's document declared `type: Policy`. `Policy` is a reserved type, so the
21
+ record demands `sources` — `ksor build`, `npm run check` and the dev server all
22
+ refused it, and steps 4 through 10 were unreachable. It is now `type: Document`,
23
+ the type the profile promises never to reserve, with a note on why and on what
24
+ an agent should do when it reaches for a reserved one.
25
+ - Step 1 scaffolds with `npx`, which emits an **npm** project, and every command
26
+ after it said `pnpm`. On that project `pnpm install && pnpm dev` fails with
27
+ `sh: next: command not found`. All sixteen commands are npm's now, and the step
28
+ that explains manager detection says which one the rest of the tutorial speaks.
29
+ - The captured outputs had been trimmed after capture, in a document whose second
30
+ paragraph promises they were "pasted as it appeared": `ksor serve`'s boot report
31
+ was missing the `trust` line it has always printed, the build outputs were
32
+ missing their timestamp, `source:` and `wrote` lines, and the port-conflict
33
+ refusal was quoted offering `pnpm serve` where it says `ksor serve`.
34
+
35
+ The walk also surfaced that `ksor init` leaves a repo with no commits, so every
36
+ reader's first build prints `source: unspecified`. Rather than hide it, the
37
+ tutorial now shows it and folds `git commit` into the approval step — which is
38
+ where provenance belongs anyway, and which lets the second build print a real
39
+ commit sha.
40
+
41
+ The tutorial also said `.mcp.json`'s "first is Neon's" and named the second
42
+ server nowhere, and said nothing about the Neon server acting on the whole Neon
43
+ account. Both are fixed here for the tutorial; the emitted scaffold's copies of
44
+ the same two defects are fixed separately.
45
+
46
+ Only the tutorial and the test that pins its prompts changed; nothing an adopter
47
+ installs behaves differently.
48
+
49
+ - ae49524: Stop a spent OpenAI balance from quarantining content and flipping a generation,
50
+ and name the right variable when a provider key is missing.
51
+
52
+ **The serious one.** `insufficient_quota` — OpenAI's answer to an exhausted
53
+ balance, which arrives as 429 like an ordinary rate limit — was classified
54
+ non-retryable, correctly, because no amount of waiting adds credit. But
55
+ "non-retryable" is what the ingest drain reads as **poison chunk**: it
56
+ binary-splits the batch down to singletons and marks each `failed`. A spent
57
+ balance arrives on _every_ chunk, so a run walked the queue quarantining
58
+ everything it touched; if the failed fraction stayed under
59
+ `MAX_FAILED_FRACTION` (2%), `generationReady` admitted it and the generation
60
+ **flipped** — publishing a record in which exactly the passages the owner had
61
+ just edited were unsearchable, `ksor ingest` exit 0, the billing reason visible
62
+ only in `chunks.embed_error`. The same event on Gemini aborts the run, so
63
+ switching provider silently changed what a spent quota does.
64
+
65
+ The drain now has three answers instead of two: retryable (abort, chunks stay
66
+ pending), **fatal** (abort the same way, but without spending five backoffs
67
+ first — the account is what is wrong, not the passage), and everything else
68
+ (binary-split to the poison chunk). `isFatal` is optional on `EmbeddingProvider`,
69
+ so a provider that cannot tell keeps the old two-kind behaviour and Gemini's
70
+ path is unchanged.
71
+
72
+ **The missing-key refusal names the variable.** `ksor serve` on an
73
+ `embedding.provider: openai` record said `embedding provider "openai" needs an
74
+ API key and none was supplied` and stopped — while `ksor serve --help`,
75
+ `env.example` and `docs/deploying.md` all named `GEMINI_API_KEY`, which that
76
+ door does not read. The registry row already held `keyEnv`; it now reaches the
77
+ operator (`— set OPENAI_API_KEY`), and all three documents describe the choice
78
+ instead of one vendor.
79
+
80
+ **`ksor calibrate`'s Gemini requirement is stated rather than papered over.**
81
+ Question synthesis is Gemini-only today, so a record embedding with
82
+ `OPENAI_API_KEY` is still refused for a Google key when calibrating through the
83
+ synthesized door. That gap is now said plainly in the refusal and in
84
+ `docs/ingesting.md`, which taught calibration without mentioning it. The
85
+ `--queries-file` door avoids it entirely.
86
+
87
+ **The OpenAI live test announces itself.** It is gated on `OPENAI_API_KEY`, no
88
+ workflow supplied one, and a false `describe.runIf` contributes nothing to a run
89
+ — so the suite its own header calls "the tripwire for vendor drift" was absent
90
+ from CI and reported as absent by nobody. It now prints `skipped — set
91
+ OPENAI_API_KEY`, the way Gemini's does, and CI passes the secret so the tripwire
92
+ arms the moment one is added.
93
+
94
+ Found by an adversarial review of this week's commits.
95
+
96
+ - ff99eb5: Hash `.ksor/people.yaml` into `build_id`, so the two surfaces of one build
97
+ cannot publish different provenance.
98
+
99
+ The phone book added in 0.0.53 rewrites the actor printed on every Owner,
100
+ Approved, Withdrawn and Trust row — `displayActor` replaces `human:contractor-a`
101
+ with "Human: Jane Doe, VP Compliance", and the identifier does not appear on the
102
+ page at all. It was hashed by nothing. `.ksor/governance.yaml` and
103
+ `.ksor/takedowns.yaml` are both in `build_id`; this one was left out, on the
104
+ stated reasoning that including it would refuse the next site build after a
105
+ spelling correction.
106
+
107
+ That is the trade critical rule 1 forbids, and the consequence was reachable
108
+ without doing anything unusual: edit a name, `pnpm check` stays green,
109
+ `ksor build` emits a byte-identical lock, and the deployed page publishes an
110
+ approver the `/md/` twin stamped with that same `build_id` contradicts. An
111
+ auditor reconciling the page against the lock finds nothing wrong, because the
112
+ string they are auditing was never covered by it.
113
+
114
+ Now: `people_sha256` joins `policy_sha256` and `ledger_sha256` in the lock and
115
+ in `build_id`; `.ksor/people.yaml` joins the inputs that move `source_commit`;
116
+ and the site's staleness gate compares it like the other three, so an edit the
117
+ lock never saw refuses with `ksor-lock-stale` naming the file. Refusing until
118
+ `ksor build` is re-run is the behaviour, not a regression — it is what every
119
+ other published byte already does.
120
+
121
+ Two things found alongside it, in the same file:
122
+
123
+ - `people.ts` claimed "duplicate keys are refused by the parser rather than
124
+ resolved by whichever came last". They were not. `uniqueKeys: true` makes the
125
+ parser RECORD a duplicate; `toJS()` still resolves last-wins, and nothing read
126
+ the errors — so two entries for one actor published the second person's name
127
+ on the first person's approval, the precise collision the map replaced a name
128
+ derivation to avoid. A duplicate now drops the whole book, and identifiers are
129
+ published instead.
130
+ - The rule lived behind a module that reads `instance.md` on import, so it could
131
+ only be tested by building a record on disk — which is why it shipped asserted
132
+ by a comment. It is now a leaf, `lib/people-rule.ts`, with the shipped
133
+ function under test.
134
+
135
+ **Upgrading:** a lock written before this refuses with `ksor-lock-invalid`
136
+ naming `people_sha256`; run `ksor build` and commit the lock it writes.
137
+
138
+ Found by an adversarial review of this week's commits.
139
+
140
+ - b45d477: Say what the scaffold's `.mcp.json` attaches to an adopter's coding agent, and
141
+ stop the README telling them to destroy it.
142
+
143
+ `ksor init` emits `.mcp.json` with two servers. The emitted README and AGENTS.md
144
+ both said "the first is Neon" and named the second nowhere — so
145
+ `agentfactory-system-of-record`, a Panaversity-operated endpoint, was wired into
146
+ every adopter's coding agent with no emitted document mentioning it. `.mcp.json`
147
+ attaches servers to the agent that OPERATES the record; a server nobody
148
+ documented is a capability nobody reviewed.
149
+
150
+ Both are now named, with what each is and that either may be deleted. The second
151
+ is described as what it is: a read-only example record that is **not** the
152
+ adopter's and that nothing in the project depends on.
153
+
154
+ The Neon step also said only that the server exists. It acts on the Neon
155
+ _account_ — an agent holding it can create and delete projects and branches — so
156
+ the README and AGENTS.md now say that before handing over a prompt that runs
157
+ against real infrastructure, and point at Neon's own documentation for the
158
+ scopes rather than paraphrasing them.
159
+
160
+ And the "Test the door with an actual agent" section told the adopter to _write_
161
+ `.mcp.json` with a file containing only `test-record` — overwriting the Neon
162
+ entry the same README depends on two sections earlier — and then closed with
163
+ "Delete `.mcp.json`, or keep it". It now shows the entry to **add**, and says not
164
+ to delete the file.
165
+
166
+ A guard derived from `mcp.json` itself asserts every server key appears in both
167
+ emitted documents, so adding a server and saying nothing fails on the server
168
+ that was added. Mutation-tested: unnaming the second server turns both red.
169
+
170
+ Found by an adversarial review of this week's commits. Whether the scaffold
171
+ should ship a second, vendor-operated MCP record at all is an owner question and
172
+ is untouched here.
173
+
174
+ - 5283084: Test infrastructure only — nothing an adopter installs behaves differently.
175
+
176
+ A skill's `description` is its trigger and nothing measured it (#30). Every
177
+ prompt the hello world tells a reader to say is now matched to a shipped skill
178
+ or recorded as needing none, and each skill's trigger phrases are pinned — so
179
+ narrowing one, the failure mode where a skill silently stops firing, goes red
180
+ naming the phrase. The model-scored half of that issue is untouched.
181
+
182
+ ## 0.0.54
183
+
184
+ ### Patch Changes
185
+
186
+ - 3f55e91: `ksor calibrate` names the free-tier path when a quota refuses it, and the
187
+ calibration text model moves to `gemini-3.7-flash`.
188
+
189
+ Walked on a real free-tier key: embedding is free of charge and a first corpus
190
+ embeds fine (23 chunks, 0 failed), but the DEFAULT calibration door writes one
191
+ probe question per sampled passage with an LLM — and the free tier allows five
192
+ generations a minute. So the documented way to turn on the product's headline
193
+ feature failed, surfacing the vendor's sentence and nothing else.
194
+
195
+ Two quotas reach that code and they need opposite answers: the generation cap is
196
+ a wall no wait clears (the remedy is `--queries-file`, the zero-LLM door), and
197
+ the embedding cap is a per-minute window (the remedy is to wait, and the usual
198
+ cause is an ingest immediately before). Both are now named, with why. A 429 this
199
+ does not recognise is re-thrown untouched — an invented remedy is worse than the
200
+ vendor's own message.
201
+
202
+ `docs/ingesting.md` documents the zero-LLM door where the reader meets the
203
+ command, including how to choose the questions: the floor is set by the weakest
204
+ one, so a vague question drags it down and a question the record cannot answer
205
+ invalidates the measurement.
206
+
207
+ The text model moves `gemini-2.5-flash` → `gemini-3.7-flash`. Cheap, unlike the
208
+ embedding model: it only writes probe questions, so nothing stored is
209
+ re-computed and no floor is invalidated — and the door is recorded beside every
210
+ number, which is what stops two measurements being compared as one experiment.
211
+
212
+ **And calibration now embeds on the patient retry.** It used the READ plane's
213
+ door, which never retries a 429 — correct for a live search, which should
214
+ degrade to keyword-only in under a second rather than stall a reader behind
215
+ backoff, and wrong for a measurement nobody is waiting on. So a free-tier key
216
+ that rate-limited mid-run refused the whole calibration. The intent stays
217
+ `query` (a floor must be measured through the label the door searches with);
218
+ only the retry policy moves, to the one `isRetryable`'s own comment describes
219
+ for batch work. Calibration's text generation already took that path, so this
220
+ was the two halves of one act disagreeing.
221
+
222
+ - dd6371b: Two things: a false claim removed from a shipped page, and the scaffold gains
223
+ `.mcp.json`.
224
+
225
+ **The false claim.** `docs/deploying.md` told adopters "The MCP surface already
226
+ applies the audience scope **per request**", under the heading of the very
227
+ requirement it does not meet. It does not: `content-gateway/src/compose.ts`
228
+ reads `KSOR_AUDIENCE` from the environment once at boot into a per-process
229
+ viewer, and the request path never touches it — `docs/authorization.md` says so
230
+ plainly ("Any caller holding a valid token gets the whole record") and
231
+ `specs/ksor/serve/spec.md` names per-request visibility filtering as out of
232
+ scope. A reader who believed the page would point every caller at one door and
233
+ serve them the restricted half. The page now says what the door does — one
234
+ viewer per door, so one process per audience — and separates the audit it does
235
+ give (a `retrieval_log` row naming the verified caller) from the authorization
236
+ it does not. A docs-truth assertion now fails on the claim itself, not merely on
237
+ a command that no longer exists.
238
+
239
+ **`.mcp.json`.** The scaffold's closed root set gains one member: the MCP
240
+ servers a coding agent may reach from the project. It ships with Neon's, which
241
+ turns the step the tool could never do for an adopter — provision a Postgres,
242
+ enable pgvector, produce a connection string — into four real tool calls
243
+ (`create_project`, `run_sql`, `create_branch`, `get_connection_string`) and one
244
+ sentence to the agent. The scaffold's README and AGENTS.md carry that sentence,
245
+ and both now say plainly which step no agent can do at any price: the embedding
246
+ API key, which no vendor mints over a protocol. Committed rather than ignored,
247
+ because both entries authenticate interactively and the file carries no secret —
248
+ stated, because pasting an API key into it would change that.
249
+
250
+ - eebd777: OpenAI joins Gemini behind the embedding seam, and the wiring stops naming one
251
+ vendor (issue #25).
252
+
253
+ The seam was already vendor-neutral in shape — `EmbeddingProvider`, the
254
+ framework's normalization and degeneracy checks, and an embedding space
255
+ identified by `modelId` + column width and never by the vendor. What was
256
+ Gemini-bound was the WIRING: `GEMINI_API_KEY` was spelled into three composition
257
+ roots, so a second provider could not obtain a key even though the registry
258
+ would happily build it. Each registry row now names its own key variable, and
259
+ the roots ask, exactly as `instance.md` names the DSN variable rather than
260
+ hardcoding it.
261
+
262
+ `provider: openai` with `model: text-embedding-3-small` and `dim: 1536` reads
263
+ `OPENAI_API_KEY`. Over `fetch`, no SDK — the same call decision 12's 2026-08-22
264
+ revision made for Gemini, and for the same reason.
265
+
266
+ Two things a live call surfaced that a stub would not. Response items carry
267
+ their own `index` and the vendor does not promise array order, so they are
268
+ sorted before the framework pairs them positionally — a shuffled response is the
269
+ same count, the same width and all finite, so every downstream check passes
270
+ while every passage carries another's vector. And an exhausted balance arrives
271
+ as **429**, the same status as a rate limit: it is now read from the vendor's
272
+ `error.type` and never retried, because five exponential backoffs do not add
273
+ credit.
274
+
275
+ Switching provider is a re-embed of the whole corpus and a re-measured
276
+ `vector_floor`. A different provider is a different embedding space, and the
277
+ invariant against copying a calibrated constant applies across vendors with more
278
+ force, not less.
279
+
280
+ Verified live against the real API on a funded key: 1536-dimension vectors, a
281
+ paraphrase at cosine 0.812 against an unrelated sentence at 0.058, and the two
282
+ intents agreeing to 0.9997 — which is the symmetry that makes the empty task
283
+ labels correct. Then through the whole plane: a real record ingested to Postgres
284
+ under `embedding_model = text-embedding-3-small`, 23 chunks, 0 failed, stored at
285
+ the declared width of 1536 and L2-normalized as the framework promises.
286
+
287
+ One more defect the live call found: `buildShippedProvider` handed EVERY
288
+ provider Gemini's task labels from global config, so an OpenAI run logged its
289
+ space as `text-embedding-3-small/d1536/RETRIEVAL_DOCUMENT` — a label that vendor
290
+ has no concept of and never received. The labels moved onto the registry row,
291
+ where a vendor's shape belongs.
292
+
293
+ - eb54871: The npm page shows what the product does, on the first screen.
294
+
295
+ It asserted the headline behaviour — a cited answer, an honest refusal — and
296
+ demonstrated it nowhere. A reader had no way to tell a real mechanism from a
297
+ prompt instruction, which is exactly the skepticism this product exists to
298
+ answer. It now shows three things, all of them real output: the admitted count
299
+ moving when a human approves a draft, the `provenance` and `governance` a search
300
+ hit carries, and an abstention envelope. It also links the hello world.
301
+
302
+ Nothing was added to the tarball; this is the README npm renders.
303
+
304
+ - 6e5ff3e: Repo documentation and a test only — nothing an adopter installs changes.
305
+
306
+ `docs/status.md` named 0.0.42 while the published package was 0.0.53. Authority
307
+ rule 3 makes that file the only authority on what is built, and it is the first
308
+ thing an evaluator's coding agent reads. It is current now, and a docs-truth
309
+ assertion holds it equal to `packages/ksor/package.json` so a Version PR cannot
310
+ bump one without the other. It also records that the full kernel walk was re-run
311
+ against 0.0.53 — it had last run against 0.0.18, thirty-five releases earlier.
312
+
313
+ - ed947c1: The deploy runbook stops ruling out the one step Vercel calls required.
314
+
315
+ `docs/deploying.md` said the silent-404 failure "does not depend on the
316
+ Application Preset". Vercel's own guide says the opposite: a project builds as
317
+ services only when the preset is `Services` AND `vercel.json` carries a
318
+ `services` key, and "if either is missing, Vercel falls back to its default
319
+ framework detection and ignores your services configuration" — which is that
320
+ failure exactly, and no file in the repository can set a project setting.
321
+
322
+ One measurement of ours disagrees with that guide and is recorded rather than
323
+ reconciled: two projects read back from the API, one `Services` and one `Other`,
324
+ both built and served. Both facts are real; guessing between them is what
325
+ produced the sentence that steered adopters away from the fix.
326
+
327
+ The scaffold's runbook now sets the preset at step 2, and ends with the three
328
+ curls that tell a live deployment from a Ready-and-404 one — `/mcp` answering
329
+ 405 is the door refusing a GET, which is how you know it is routed at all.
330
+
3
331
  ## 0.0.53
4
332
 
5
333
  ### Patch Changes
package/README.md CHANGED
@@ -16,6 +16,36 @@ pnpm install
16
16
  pnpm dev # the site, live at http://localhost:3000
17
17
  ```
18
18
 
19
+ Then write a document and publish it:
20
+
21
+ ```console
22
+ $ pnpm exec ksor build
23
+ ksor build: 6 document(s), 5 admitted to a machine surface
24
+ ```
25
+
26
+ **Six documents, five admitted.** The one you just wrote is a `draft`, so it
27
+ reaches nothing an AI agent reads — not `llms.txt`, not the markdown twins —
28
+ until a human approves it. Approve it and the count moves. That is the whole
29
+ product, and it costs nothing: no database, no API key, no account.
30
+
31
+ Climb one rung and an agent asks the record a question. The answer carries
32
+ where it came from:
33
+
34
+ ```json
35
+ "provenance": { "stable_id": "knowledge/refund-policy", "generation": 1 },
36
+ "governance": { "status": "stable", "approval": { "by": "human:you" } }
37
+ ```
38
+
39
+ …and a question the record does not cover is declined rather than guessed at:
40
+
41
+ ```json
42
+ { "ok": false, "abstained": true, "gate": { "floor": 0.622 } }
43
+ ```
44
+
45
+ **[Hello world](https://github.com/panaversity/ksor/blob/main/docs/tutorials/00-hello-world.md)**
46
+ walks all of that in about fifteen minutes. Every command and output in it was
47
+ run and pasted as it appeared — including the ones above.
48
+
19
49
  One command emits a complete governed project: the record (`knowledge/`,
20
50
  plain CommonMark), a working documentation site with hot reload, offline
21
51
  search and `llms.txt`, adopter CI, a dependency-free format checker
@@ -10892,7 +10892,7 @@ var require_public_api = /* @__PURE__ */ __commonJSMin(((exports) => {
10892
10892
  exports.stringify = stringify;
10893
10893
  }));
10894
10894
  //#endregion
10895
- //#region ../content/dist/check-DNWlQuBg.mjs
10895
+ //#region ../content/dist/check-6fB_kR4G.mjs
10896
10896
  var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => {
10897
10897
  var composer = require_composer();
10898
10898
  var Document = require_Document();
@@ -12150,6 +12150,7 @@ function changedFields(before, after) {
12150
12150
  const CONTROL_FILES = [
12151
12151
  "instance.md",
12152
12152
  ".ksor/governance.yaml",
12153
+ ".ksor/people.yaml",
12153
12154
  ".ksor/takedowns.yaml"
12154
12155
  ];
12155
12156
  /** Files the operating system writes behind the author's back: ignored, never reported. */
@@ -13714,7 +13715,7 @@ function checkAgainstPolicy(concept, policy, refusals) {
13714
13715
  }
13715
13716
  }
13716
13717
  //#endregion
13717
- //#region ../content/dist/record-DnMnZelb.mjs
13718
+ //#region ../content/dist/record-Cxw0SUG3.mjs
13718
13719
  const hex64 = string().regex(/^[0-9a-f]{64}$/, "a sha256 hex digest");
13719
13720
  const viewerList = array(string().min(1));
13720
13721
  const lockSchema = object({
@@ -13732,6 +13733,7 @@ const lockSchema = object({
13732
13733
  drafts: _enum(["hidden", "shown"]),
13733
13734
  instance_sha256: hex64,
13734
13735
  policy_sha256: hex64,
13736
+ people_sha256: hex64,
13735
13737
  ledger_sha256: hex64,
13736
13738
  ledger_entries: array(object({
13737
13739
  id: string().min(1),
@@ -13928,6 +13930,7 @@ const INPUTS = [
13928
13930
  "knowledge",
13929
13931
  "instance.md",
13930
13932
  ".ksor/governance.yaml",
13933
+ ".ksor/people.yaml",
13931
13934
  ".ksor/takedowns.yaml"
13932
13935
  ];
13933
13936
  function gitFacts(root) {