@panaversity/ksor 0.0.54 → 0.0.56

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 (33) hide show
  1. package/CHANGELOG.md +369 -0
  2. package/dist/checker/check-main.mjs +5 -2
  3. package/dist/cli.mjs +56 -17
  4. package/dist/{gateway-api-uhx2l1kC-C2BAxISt.mjs → gateway-api-C0vL3oOK-D24n786A.mjs} +25 -4
  5. package/dist/gateway.d.mts +2 -2
  6. package/dist/gateway.mjs +1 -1
  7. package/docs/deploying.md +7 -1
  8. package/docs/ingesting.md +10 -5
  9. package/package.json +1 -1
  10. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +129 -84
  11. package/templates/scaffold/.agents/skills/add-sources/verify.mjs +45 -0
  12. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +18 -46
  13. package/templates/scaffold/.agents/skills/format-checker/check.mjs +3 -0
  14. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +16 -8
  15. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +129 -84
  16. package/templates/scaffold/.claude/skills/add-sources/verify.mjs +45 -0
  17. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +18 -46
  18. package/templates/scaffold/.claude/skills/format-checker/check.mjs +3 -0
  19. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +16 -8
  20. package/templates/scaffold/AGENTS.md +21 -10
  21. package/templates/scaffold/README.md +47 -16
  22. package/templates/scaffold/env.example +6 -1
  23. package/templates/scaffold/gitignore +4 -3
  24. package/templates/scaffold/system/site/lib/lock.ts +8 -1
  25. package/templates/scaffold/system/site/lib/people-rule.ts +56 -0
  26. package/templates/scaffold/system/site/lib/people.ts +5 -24
  27. package/templates/scaffold/system/site/lib/stage-knowledge.ts +2 -0
  28. package/templates/scaffold/system/site/record/load.ts +11 -1
  29. package/templates/scaffold/system/site/record/lock.ts +12 -0
  30. package/templates/scaffold/.agents/skills/make-slides/SKILL.md +0 -162
  31. package/templates/scaffold/.agents/skills/make-summary/SKILL.md +0 -153
  32. package/templates/scaffold/.claude/skills/make-slides/SKILL.md +0 -162
  33. package/templates/scaffold/.claude/skills/make-summary/SKILL.md +0 -153
package/CHANGELOG.md CHANGED
@@ -1,5 +1,374 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.56
4
+
5
+ ### Patch Changes
6
+
7
+ - bf35e2f: `add-sources` 2.0.0: a file or a person, one skill — with a check the agent
8
+ runs instead of a rule it is asked to follow.
9
+
10
+ Two issues asked for two paths into the record. #31: an owner with a folder of
11
+ PDFs had no path — the skill stated the rules and converted nothing. #50: an
12
+ owner whose knowledge is only in their head had no path — the interview scoped
13
+ the record and stopped, leaving `knowledge/` full of samples about KSoR.
14
+
15
+ **They are one skill, not two.** The record draws no line between the kinds:
16
+ an interview attestation in `sources[].resource` passes `ksor build` today,
17
+ the fidelity rules read the same for both, and a real owner has BOTH — the
18
+ policy PDF and the exception everyone knows that the PDF never mentions. So
19
+ the person step runs after every file: "what does this not cover?" is the
20
+ question that finds the pages nobody wrote. A sibling skill would have made
21
+ the agent choose before it knew, and #50's own three-way trigger collision
22
+ vanishes.
23
+
24
+ **The source is a file.** Extract first, into a scratch file outside
25
+ `knowledge/` — the skill names the extractor per format (`pdftotext`,
26
+ `pandoc`, macOS `textutil`, `markitdown`) and what to do with none on `PATH`:
27
+ read the file directly and SAY the verification that follows is weaker. An
28
+ empty extraction is a scanned image, and the skill stops and tells the owner
29
+ rather than OCR and hope. Then decide the shape of the record, convert to
30
+ CommonMark a person would have written, name the source precisely, and run
31
+ the shipped check:
32
+
33
+ ```
34
+ node .agents/skills/add-sources/verify.mjs /tmp/in.txt knowledge/<path>.md
35
+ ```
36
+
37
+ `verify.mjs` — plain Node, no dependencies, the owner's to keep — lists every
38
+ number, date, threshold, code and capitalised name in the document's body that
39
+ does not appear in the extraction, case-folded and whitespace-collapsed.
40
+ Frontmatter and footnote ids are exempt, because they are the agent's words by
41
+ design. It is a floor, and says so: a value that passes was in the source; it
42
+ cannot see a value that was dropped, and it cannot tell a paraphrase from an
43
+ invention. Model-driven conversion is highest-fidelity for layout and
44
+ lowest for exact values, and this is the mechanical half of "copy load-bearing
45
+ values exactly".
46
+
47
+ **The source is a person.** Ask one question at a time in their words — who
48
+ triggers it, the steps, who approves and at what threshold, what goes wrong,
49
+ the exception — until someone who was not in the room could act on it. Draft
50
+ as the record, not a transcript; anything unconfirmed is an `Open question:`
51
+ line, never prose. The attestation goes in `sources` (who, role, instant,
52
+ conducted by) — there is no `provenance:` key, and no transcript is kept.
53
+ Two people describing one process differently stay two cited statements.
54
+
55
+ **Both end the same way, and the ending is new.** Read it back on `pnpm dev`,
56
+ then ask the owner to approve and write down what they said. A draft reaches
57
+ no machine surface, so a skill that stopped at the draft left every `llms.txt`
58
+ and every door empty — found on the journey walk: `1 document(s), 0 admitted`.
59
+
60
+ The rules that were restated here (placement, frontmatter, audience,
61
+ deprecation) now point at the emitted AGENTS.md instead. The trigger test that
62
+ asserted "no skill claims dictated knowledge" flips: add-sources claims it by
63
+ name, and no other skill may. Eleven cases hold `verify.mjs` to what it does
64
+ and, in three of them, to what it does not claim.
65
+
66
+ - 73530e6: Prune the scaffold's skills to the three that make a record, and fix the two
67
+ seams every adopter hits on the way to one.
68
+
69
+ **Removed: `make-slides` and `make-summary`.** They were 45% of all shipped
70
+ skill text and 21.6% identical to each other — their own commit says
71
+ "make-summary is make-slides' discipline applied to prose". A companion is
72
+ downstream of a record existing and invisible to the agent surface (no route,
73
+ no `llms.txt` line, no MCP node); no fixture and no tutorial ever fired either;
74
+ and neither was ever shown to beat its absence, which is the bar AGENTS.md sets
75
+ for keeping a skill at all. Their one real rule — a card may only say what its
76
+ document says — already lives in the emitted AGENTS.md, and the site renders
77
+ companions exactly as before. Verified: the emitted checker passes with both
78
+ gone. `format-checker/SKILL.md` is cut to what AGENTS.md does not say; the
79
+ program it names is unchanged.
80
+
81
+ **Fixed: `intake-interview` was contradicting itself in its trigger.** The
82
+ always-resident description promised "seven questions"; the body has asked
83
+ three since 2026-08-26. The body handed off to "question 4" and "question 5",
84
+ neither of which exists, and claimed `add-sources` writes `verified:` entries —
85
+ nothing does. 1.6.0 says three, hands off to add-sources with whatever material
86
+ the owner has, and drops the false claim.
87
+
88
+ **Fixed: the recommended path turned a green record red.** Walked on the
89
+ published package:
90
+
91
+ - The README said run the interview, then "delete each starter as your own
92
+ knowledge arrives". Delete the five first and the build refuses
93
+ `ksor-record-empty` and writes nothing — a slug named by no document an
94
+ adopter reads. The README now says to write and approve one document of
95
+ your own before the last starter goes, and names the refusal.
96
+ - The hello-world tutorial approves as `human:you`. The interview then retires
97
+ `human:you` from the policy, and the tutorial's own document — still approved
98
+ by an actor the policy no longer names — refuses `ksor-approver-unauthorised`.
99
+ The interview now re-attributes every act recorded under the placeholder to
100
+ the owner's handle in the same change: it is the same person.
101
+
102
+ **Consolidated:** "the record says only what its source says — a gap is an
103
+ open question, never filled from general knowledge" lived only inside
104
+ `add-sources`; it is now stated once in the emitted AGENTS.md where every
105
+ other writing rule is.
106
+
107
+ Two deterministic gates hold all of this: a skill-consistency lint (a trigger's
108
+ question count matches its body; every "question N" resolves; every refusal
109
+ slug a document names is one the product raises; every skill a document names
110
+ ships) and a journey walk against the built CLI (interview → one draft →
111
+ approve → delete starters → retire the starter actor, plus the exact state
112
+ each refusal fires on). Every lint assertion was mutation-tested.
113
+
114
+ Found by four independent reviews of the plan to build a system of record,
115
+ before building any of it.
116
+
117
+ - ab6a3ed: The agent tier: a shipped skill, run by a real coding agent, with the skill
118
+ and without it — the comparison AGENTS.md has always demanded and nothing had
119
+ ever run (issue #30).
120
+
121
+ `pnpm test:agent` scaffolds a fresh record, installs it, drops a real two-page
122
+ PDF in `src/`, and hands `claude -p` the prompt tutorial 2 hands the reader.
123
+ Once with `add-sources` present, once with it removed. What the agent leaves
124
+ behind is graded, and the split is the Testing contract's own: deterministic
125
+ behavioural graders GATE the with-skill arm — exactly one new document, under
126
+ `finance/`; `.ksor/*` and `instance.md` untouched; the record builds; `status:
127
+ draft`, `sources` present, no `id:`/`name:`; page furniture gone; every
128
+ number, date and name in the body found in the extraction by the shipped
129
+ `verify.mjs`. Cost, turns, duration and the baseline arm are REPORTED, so the
130
+ delta is visible and a skill that stops winning is seen.
131
+
132
+ "Checker passes" is deliberately not a grader: while this was being designed a
133
+ baseline run passed the checker by hand-authoring `index.md` and editing
134
+ `.ksor/people.yaml` — the worse behaviour scoring better. Files touched is the
135
+ discriminating assertion.
136
+
137
+ It spends model tokens, so it is gated like the database tier: on
138
+ `ANTHROPIC_API_KEY` in CI (a repository secret the owner has not yet added; the
139
+ tier runs and reports itself skipped until then) or a logged-in `claude`
140
+ locally, pins a mid-tier model by default (a one-word reply on the default
141
+ model measured $0.25), and runs from `skill-evals.yml` on push to main and by
142
+ hand — never per pull request.
143
+
144
+ What it cannot measure is written in the suite rather than implied: a
145
+ conversational skill needs a scripted owner, "reads as a finished page" needs
146
+ a browser, and the adopter's own model is whatever they run.
147
+
148
+ Decision 31 records the three choices this week made about the skill surface —
149
+ pruned to three, one skill for a file and a person, and this harness shape
150
+ over the Python trigger script that was proposed and measured wanting.
151
+
152
+ - c34cc3a: Tutorial 2, _Make it yours_: the walk from hello world's record to one that is
153
+ only the owner's — every output run and pasted as it appeared.
154
+
155
+ The intake interview and what it does to the placeholder approver; one policy
156
+ brought in from a real PDF, with the shipped check catching the one number the
157
+ conversion got wrong; one procedure that only ever lived in someone's head,
158
+ written with the thing they were not sure of as an open question rather than
159
+ prose; the read-back on the site and the approval act; then the samples go and
160
+ the tool that approved them leaves the policy. Two refusals do work on the way,
161
+ and the tutorial says exactly which state each fires on.
162
+
163
+ The prompt-accounting test now covers both tutorials from one table, so a new
164
+ prompt in either fails until someone names the skill that answers it. Only that
165
+ test changes under `packages/`; nothing an adopter installs behaves differently.
166
+
167
+ - 87a3542: Correct a claim the tutorial made about `build_id`, and guard the general rule.
168
+
169
+ `buildIdOf` hashes `ksor_version` along with the record — deliberately, because
170
+ "what produced this" is part of what a publication is. So a captured `build_id`
171
+ is correct for exactly one release, and the sentence 0.0.54's tutorial fix added
172
+ — "Your timestamp will differ; the `build_id` will not" — was already false when
173
+ 0.0.55 published it. A reader on any later ksor sees a hash that does not match
174
+ theirs and nothing saying why.
175
+
176
+ Found by walking the published package rather than by reading the diff: the same
177
+ practice that caught the tutorial being uncompletable caught the correction being
178
+ wrong.
179
+
180
+ The tutorial now says the id carries the toolchain, names the version its
181
+ outputs were captured on, and points at the reproducibility a reader can
182
+ actually check — run `ksor build` twice on one tree and the id is identical.
183
+ Both captured blocks are re-taken from a 0.0.55 walk.
184
+
185
+ A guard in `docs-truth.integration.test.ts` holds the general rule rather than
186
+ the sentence: a document printing a concrete `build_id` must say what moves one,
187
+ within 700 characters of the id. It is PROXIMITY rather than presence — the
188
+ first version asked whether "toolchain" appeared anywhere in the file, the file
189
+ already used the word once for an unrelated reason, and removing the caveat left
190
+ it green. Caught by mutation, and the tightened version immediately found a
191
+ second uncaveated id in the same document.
192
+
193
+ ## 0.0.55
194
+
195
+ ### Patch Changes
196
+
197
+ - 4ecf549: Test infrastructure only — nothing an adopter installs behaves differently.
198
+
199
+ The behavioural evals scored a missing `top_cosine` as `-1`. When a provider
200
+ rate-limits, the read plane degrades to keyword-only by design, so searches
201
+ answer with no score — and the assertions then compared sentinels, reporting a
202
+ vendor outage as "the abstention floor is broken". Four CI failures in a day
203
+ read that way before anyone looked past the assertion. A missing score now
204
+ refuses, naming the cause, and never invents the number that is absent.
205
+
206
+ - e476197: Fix the hello-world tutorial, which could not be completed as written.
207
+
208
+ Three defects, all found by walking the published 0.0.54 rather than by reading:
209
+
210
+ - Step 3's document declared `type: Policy`. `Policy` is a reserved type, so the
211
+ record demands `sources` — `ksor build`, `npm run check` and the dev server all
212
+ refused it, and steps 4 through 10 were unreachable. It is now `type: Document`,
213
+ the type the profile promises never to reserve, with a note on why and on what
214
+ an agent should do when it reaches for a reserved one.
215
+ - Step 1 scaffolds with `npx`, which emits an **npm** project, and every command
216
+ after it said `pnpm`. On that project `pnpm install && pnpm dev` fails with
217
+ `sh: next: command not found`. All sixteen commands are npm's now, and the step
218
+ that explains manager detection says which one the rest of the tutorial speaks.
219
+ - The captured outputs had been trimmed after capture, in a document whose second
220
+ paragraph promises they were "pasted as it appeared": `ksor serve`'s boot report
221
+ was missing the `trust` line it has always printed, the build outputs were
222
+ missing their timestamp, `source:` and `wrote` lines, and the port-conflict
223
+ refusal was quoted offering `pnpm serve` where it says `ksor serve`.
224
+
225
+ The walk also surfaced that `ksor init` leaves a repo with no commits, so every
226
+ reader's first build prints `source: unspecified`. Rather than hide it, the
227
+ tutorial now shows it and folds `git commit` into the approval step — which is
228
+ where provenance belongs anyway, and which lets the second build print a real
229
+ commit sha.
230
+
231
+ The tutorial also said `.mcp.json`'s "first is Neon's" and named the second
232
+ server nowhere, and said nothing about the Neon server acting on the whole Neon
233
+ account. Both are fixed here for the tutorial; the emitted scaffold's copies of
234
+ the same two defects are fixed separately.
235
+
236
+ Only the tutorial and the test that pins its prompts changed; nothing an adopter
237
+ installs behaves differently.
238
+
239
+ - ae49524: Stop a spent OpenAI balance from quarantining content and flipping a generation,
240
+ and name the right variable when a provider key is missing.
241
+
242
+ **The serious one.** `insufficient_quota` — OpenAI's answer to an exhausted
243
+ balance, which arrives as 429 like an ordinary rate limit — was classified
244
+ non-retryable, correctly, because no amount of waiting adds credit. But
245
+ "non-retryable" is what the ingest drain reads as **poison chunk**: it
246
+ binary-splits the batch down to singletons and marks each `failed`. A spent
247
+ balance arrives on _every_ chunk, so a run walked the queue quarantining
248
+ everything it touched; if the failed fraction stayed under
249
+ `MAX_FAILED_FRACTION` (2%), `generationReady` admitted it and the generation
250
+ **flipped** — publishing a record in which exactly the passages the owner had
251
+ just edited were unsearchable, `ksor ingest` exit 0, the billing reason visible
252
+ only in `chunks.embed_error`. The same event on Gemini aborts the run, so
253
+ switching provider silently changed what a spent quota does.
254
+
255
+ The drain now has three answers instead of two: retryable (abort, chunks stay
256
+ pending), **fatal** (abort the same way, but without spending five backoffs
257
+ first — the account is what is wrong, not the passage), and everything else
258
+ (binary-split to the poison chunk). `isFatal` is optional on `EmbeddingProvider`,
259
+ so a provider that cannot tell keeps the old two-kind behaviour and Gemini's
260
+ path is unchanged.
261
+
262
+ **The missing-key refusal names the variable.** `ksor serve` on an
263
+ `embedding.provider: openai` record said `embedding provider "openai" needs an
264
+ API key and none was supplied` and stopped — while `ksor serve --help`,
265
+ `env.example` and `docs/deploying.md` all named `GEMINI_API_KEY`, which that
266
+ door does not read. The registry row already held `keyEnv`; it now reaches the
267
+ operator (`— set OPENAI_API_KEY`), and all three documents describe the choice
268
+ instead of one vendor.
269
+
270
+ **`ksor calibrate`'s Gemini requirement is stated rather than papered over.**
271
+ Question synthesis is Gemini-only today, so a record embedding with
272
+ `OPENAI_API_KEY` is still refused for a Google key when calibrating through the
273
+ synthesized door. That gap is now said plainly in the refusal and in
274
+ `docs/ingesting.md`, which taught calibration without mentioning it. The
275
+ `--queries-file` door avoids it entirely.
276
+
277
+ **The OpenAI live test announces itself.** It is gated on `OPENAI_API_KEY`, no
278
+ workflow supplied one, and a false `describe.runIf` contributes nothing to a run
279
+ — so the suite its own header calls "the tripwire for vendor drift" was absent
280
+ from CI and reported as absent by nobody. It now prints `skipped — set
281
+ OPENAI_API_KEY`, the way Gemini's does, and CI passes the secret so the tripwire
282
+ arms the moment one is added.
283
+
284
+ Found by an adversarial review of this week's commits.
285
+
286
+ - ff99eb5: Hash `.ksor/people.yaml` into `build_id`, so the two surfaces of one build
287
+ cannot publish different provenance.
288
+
289
+ The phone book added in 0.0.53 rewrites the actor printed on every Owner,
290
+ Approved, Withdrawn and Trust row — `displayActor` replaces `human:contractor-a`
291
+ with "Human: Jane Doe, VP Compliance", and the identifier does not appear on the
292
+ page at all. It was hashed by nothing. `.ksor/governance.yaml` and
293
+ `.ksor/takedowns.yaml` are both in `build_id`; this one was left out, on the
294
+ stated reasoning that including it would refuse the next site build after a
295
+ spelling correction.
296
+
297
+ That is the trade critical rule 1 forbids, and the consequence was reachable
298
+ without doing anything unusual: edit a name, `pnpm check` stays green,
299
+ `ksor build` emits a byte-identical lock, and the deployed page publishes an
300
+ approver the `/md/` twin stamped with that same `build_id` contradicts. An
301
+ auditor reconciling the page against the lock finds nothing wrong, because the
302
+ string they are auditing was never covered by it.
303
+
304
+ Now: `people_sha256` joins `policy_sha256` and `ledger_sha256` in the lock and
305
+ in `build_id`; `.ksor/people.yaml` joins the inputs that move `source_commit`;
306
+ and the site's staleness gate compares it like the other three, so an edit the
307
+ lock never saw refuses with `ksor-lock-stale` naming the file. Refusing until
308
+ `ksor build` is re-run is the behaviour, not a regression — it is what every
309
+ other published byte already does.
310
+
311
+ Two things found alongside it, in the same file:
312
+
313
+ - `people.ts` claimed "duplicate keys are refused by the parser rather than
314
+ resolved by whichever came last". They were not. `uniqueKeys: true` makes the
315
+ parser RECORD a duplicate; `toJS()` still resolves last-wins, and nothing read
316
+ the errors — so two entries for one actor published the second person's name
317
+ on the first person's approval, the precise collision the map replaced a name
318
+ derivation to avoid. A duplicate now drops the whole book, and identifiers are
319
+ published instead.
320
+ - The rule lived behind a module that reads `instance.md` on import, so it could
321
+ only be tested by building a record on disk — which is why it shipped asserted
322
+ by a comment. It is now a leaf, `lib/people-rule.ts`, with the shipped
323
+ function under test.
324
+
325
+ **Upgrading:** a lock written before this refuses with `ksor-lock-invalid`
326
+ naming `people_sha256`; run `ksor build` and commit the lock it writes.
327
+
328
+ Found by an adversarial review of this week's commits.
329
+
330
+ - b45d477: Say what the scaffold's `.mcp.json` attaches to an adopter's coding agent, and
331
+ stop the README telling them to destroy it.
332
+
333
+ `ksor init` emits `.mcp.json` with two servers. The emitted README and AGENTS.md
334
+ both said "the first is Neon" and named the second nowhere — so
335
+ `agentfactory-system-of-record`, a Panaversity-operated endpoint, was wired into
336
+ every adopter's coding agent with no emitted document mentioning it. `.mcp.json`
337
+ attaches servers to the agent that OPERATES the record; a server nobody
338
+ documented is a capability nobody reviewed.
339
+
340
+ Both are now named, with what each is and that either may be deleted. The second
341
+ is described as what it is: a read-only example record that is **not** the
342
+ adopter's and that nothing in the project depends on.
343
+
344
+ The Neon step also said only that the server exists. It acts on the Neon
345
+ _account_ — an agent holding it can create and delete projects and branches — so
346
+ the README and AGENTS.md now say that before handing over a prompt that runs
347
+ against real infrastructure, and point at Neon's own documentation for the
348
+ scopes rather than paraphrasing them.
349
+
350
+ And the "Test the door with an actual agent" section told the adopter to _write_
351
+ `.mcp.json` with a file containing only `test-record` — overwriting the Neon
352
+ entry the same README depends on two sections earlier — and then closed with
353
+ "Delete `.mcp.json`, or keep it". It now shows the entry to **add**, and says not
354
+ to delete the file.
355
+
356
+ A guard derived from `mcp.json` itself asserts every server key appears in both
357
+ emitted documents, so adding a server and saying nothing fails on the server
358
+ that was added. Mutation-tested: unnaming the second server turns both red.
359
+
360
+ Found by an adversarial review of this week's commits. Whether the scaffold
361
+ should ship a second, vendor-operated MCP record at all is an owner question and
362
+ is untouched here.
363
+
364
+ - 5283084: Test infrastructure only — nothing an adopter installs behaves differently.
365
+
366
+ A skill's `description` is its trigger and nothing measured it (#30). Every
367
+ prompt the hello world tells a reader to say is now matched to a shipped skill
368
+ or recorded as needing none, and each skill's trigger phrases are pinned — so
369
+ narrowing one, the failure mode where a skill silently stops firing, goes red
370
+ naming the phrase. The model-scored half of that issue is untouched.
371
+
3
372
  ## 0.0.54
4
373
 
5
374
  ### Patch Changes
@@ -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) {
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as resolveCommand, r as verbs, t as exitCodes } from "./src-dqpI-p1a.mjs";
3
- import { A as parseViewer, B as tallyHandlers, C as contentPoolMin, D as outlineHandler, E as keyRingFromEnv, F as recordIsUndescribed, G as z$1, H as validateViewer, I as runProbe$1, L as searchHandler, M as prewarmPool, N as providerKeyEnv$1, O as parseInstanceText$1, P as readHandler, R as servingPolicy$1, S as contentPool$1, T as instancePathOf$1, U as withPgRetry$1, V as tlsPosture, W as withProbeDeadline$1, _ as assertGovernanceServable$1, a as GovernanceGateError$1, b as checkEmbeddingSpace$1, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, g as TextSearchConfigMismatch, h as TRUST_TIERS$1, i as FLOOR, j as pooledEndpointFor, k as parseTrustFloor, l as MissingProviderKeyError$1, m as SchemaVersionError, n as ContentStoreError$1, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, r as EmbeddingSpaceMismatch$1, t as AudienceError$1, u as OUTLINE_OUTPUT, v as assertSchemaCompatible, w as embedQueryVlit, x as composeInstructions, y as buildShippedProvider$1, z as storedTextSearchConfig } from "./gateway-api-uhx2l1kC-C2BAxISt.mjs";
3
+ import { A as parseViewer, B as tallyHandlers, C as contentPoolMin, D as outlineHandler, E as keyRingFromEnv, F as recordIsUndescribed, G as z$1, H as validateViewer, I as runProbe$1, L as searchHandler, M as prewarmPool, N as providerKeyEnv$1, O as parseInstanceText$1, P as readHandler, R as servingPolicy$1, S as contentPool$1, T as instancePathOf$1, U as withPgRetry$1, V as tlsPosture, W as withProbeDeadline$1, _ as assertGovernanceServable$1, a as GovernanceGateError$1, b as checkEmbeddingSpace$1, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, g as TextSearchConfigMismatch, h as TRUST_TIERS$1, i as FLOOR, j as pooledEndpointFor, k as parseTrustFloor, l as MissingProviderKeyError$1, m as SchemaVersionError, n as ContentStoreError$1, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, r as EmbeddingSpaceMismatch$1, t as AudienceError$1, u as OUTLINE_OUTPUT, v as assertSchemaCompatible, w as embedQueryVlit, x as composeInstructions, y as buildShippedProvider$1, z as storedTextSearchConfig } from "./gateway-api-C0vL3oOK-D24n786A.mjs";
4
4
  import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { InMemoryTransport, LATEST_PROTOCOL_VERSION, createMcpHandler } from "@modelcontextprotocol/server";
@@ -17,7 +17,7 @@ import { Document, YAMLParseError, isCollection, isMap, isPair, isSeq, parseAllD
17
17
  import { parseArgs } from "node:util";
18
18
  import { readFile } from "node:fs/promises";
19
19
  import { execFileSync, spawnSync } from "node:child_process";
20
- //#region ../content-gateway/dist/main-BHQDvsKA.mjs
20
+ //#region ../content-gateway/dist/main-BoKhKcP0.mjs
21
21
  /**
22
22
  * The default registration — and the ORIGINAL of the file `ksor init` emits.
23
23
  *
@@ -1533,7 +1533,7 @@ async function main$1(version = GATEWAY_VERSION) {
1533
1533
  }
1534
1534
  }
1535
1535
  //#endregion
1536
- //#region ../content/dist/check-DNWlQuBg.mjs
1536
+ //#region ../content/dist/check-6fB_kR4G.mjs
1537
1537
  /**
1538
1538
  * One reader for the control files beside the bundle (`.ksor/*.yaml`): the
1539
1539
  * same parser posture as a concept's frontmatter — one document, unique keys,
@@ -2825,6 +2825,7 @@ const LEDGER_HEADER$1 = "# The takedown ledger (record spec §5): append-only, w
2825
2825
  const CONTROL_FILES = [
2826
2826
  "instance.md",
2827
2827
  ".ksor/governance.yaml",
2828
+ ".ksor/people.yaml",
2828
2829
  ".ksor/takedowns.yaml"
2829
2830
  ];
2830
2831
  /** Files the operating system writes behind the author's back: ignored, never reported. */
@@ -4709,7 +4710,7 @@ async function withPgRetry(op, options = {}) {
4709
4710
  throw lastError;
4710
4711
  }
4711
4712
  //#endregion
4712
- //#region ../content/dist/commands-DM5TajmH.mjs
4713
+ //#region ../content/dist/commands-CO9-bQnb.mjs
4713
4714
  /**
4714
4715
  * EVAL-LOCKED constants, quarried verbatim from the oracle
4715
4716
  * (sor-agentfactory @ b554f91, config.py) — changing any of these is a
@@ -6228,6 +6229,14 @@ function isRetryable(exc) {
6228
6229
  * project stays rate-limited on the next second, so a search degrades to
6229
6230
  * keyword-only now rather than stalling a reader behind backoff.
6230
6231
  */
6232
+ /**
6233
+ * An ACCOUNT-level failure: no amount of waiting and no other passage changes
6234
+ * it. The drain must abort on this rather than quarantine, because the chunk
6235
+ * it happened to be holding is not what is wrong — see `ingest/worker.ts`.
6236
+ */
6237
+ function isFatal(exc) {
6238
+ return exc instanceof OpenAiHttpError && exc.kind === "insufficient_quota";
6239
+ }
6231
6240
  function isRetryableQuery(exc) {
6232
6241
  if (isTransportBlip(exc)) return true;
6233
6242
  const status = httpStatusOf(exc);
@@ -6278,6 +6287,9 @@ var OpenAiEmbeddingProvider = class {
6278
6287
  isRetryableQuery(exc) {
6279
6288
  return isRetryableQuery(exc);
6280
6289
  }
6290
+ isFatal(exc) {
6291
+ return isFatal(exc);
6292
+ }
6281
6293
  };
6282
6294
  /**
6283
6295
  * The embedding-provider registry — a plain object, NOT any discovery
@@ -6304,10 +6316,20 @@ var OpenAiEmbeddingProvider = class {
6304
6316
  */
6305
6317
  var MissingProviderKeyError = class extends Error {
6306
6318
  providerName;
6307
- constructor(providerName) {
6308
- super(`embedding provider ${JSON.stringify(providerName)} needs an API key and none was supplied`);
6319
+ keyEnv;
6320
+ /**
6321
+ * `keyEnv` is not decoration. The message named the PROVIDER and nothing
6322
+ * else, so an operator whose `ksor serve` exited 3 on an OpenAI record was
6323
+ * told "provider openai needs an API key" and left to guess which variable —
6324
+ * while `ksor serve --help`, `env.example` and `docs/deploying.md` all named
6325
+ * `GEMINI_API_KEY`, which the door does not read (review, 2026-09-01). The
6326
+ * registry row already held the answer; this is it reaching the operator.
6327
+ */
6328
+ constructor(providerName, keyEnv = null) {
6329
+ super(`embedding provider ${JSON.stringify(providerName)} needs an API key and none was supplied` + (keyEnv === null ? "" : ` — set ${keyEnv}`));
6309
6330
  this.name = "MissingProviderKeyError";
6310
6331
  this.providerName = providerName;
6332
+ this.keyEnv = keyEnv;
6311
6333
  }
6312
6334
  };
6313
6335
  const PROVIDERS = {
@@ -6369,7 +6391,7 @@ function providerKeyEnv(name) {
6369
6391
  */
6370
6392
  function buildShippedProvider(name, opts) {
6371
6393
  const entry = entryFor(name);
6372
- if (entry.needsApiKey && !opts.apiKey) throw new MissingProviderKeyError(name);
6394
+ if (entry.needsApiKey && !opts.apiKey) throw new MissingProviderKeyError(name, entry.keyEnv);
6373
6395
  return entry.build({
6374
6396
  apiKey: opts.apiKey ?? "",
6375
6397
  modelId: opts.modelId ?? "gemini-embedding-001",
@@ -9275,6 +9297,7 @@ async function drain(pending, io) {
9275
9297
  literals = await io.embedBatch(batch.map(([, text]) => text));
9276
9298
  } catch (exc) {
9277
9299
  if (io.isRetryable(exc)) throw exc;
9300
+ if (io.isFatal?.(exc) === true) throw exc;
9278
9301
  if (batch.length === 1) {
9279
9302
  await io.markFailed(failureReason(exc), batch[0][0]);
9280
9303
  failed += 1;
@@ -9826,7 +9849,8 @@ async function buildGeneration(pool, instance, options) {
9826
9849
  markFailed: (reason, chunkId) => runIngest(pool, tenant, async (c) => {
9827
9850
  await c.query(FAIL_SQL, [reason, chunkId]);
9828
9851
  }),
9829
- isRetryable: (exc) => provider.isRetryable(exc)
9852
+ isRetryable: (exc) => provider.isRetryable(exc),
9853
+ isFatal: (exc) => provider.isFatal?.(exc) === true
9830
9854
  });
9831
9855
  log(`embedded ${embedded}, failed ${failed}`);
9832
9856
  const fin = await runIngest(pool, tenant, async (c) => {
@@ -10633,7 +10657,7 @@ async function calibrateCommand(args) {
10633
10657
  let textGenerator = null;
10634
10658
  if (queries === null) {
10635
10659
  const apiKey = process.env["GEMINI_API_KEY"];
10636
- if (apiKey === void 0 || apiKey === "") return refuse$2("bad-args", "the synthesized door needs GEMINI_API_KEY (it writes one probe question per sampled passage) — or calibrate with zero LLM: --queries-file PATH (one in-corpus question per line)");
10660
+ if (apiKey === void 0 || apiKey === "") return refuse$2("bad-args", "the synthesized door needs GEMINI_API_KEY (it writes one probe question per sampled passage) — or calibrate with zero LLM: --queries-file PATH (one in-corpus question per line).\n note: this is the TEXT generator, not the embedding provider. A record on `embedding.provider: openai` still embeds with OPENAI_API_KEY; only question synthesis is Gemini-only today, and --queries-file avoids it entirely");
10637
10661
  textGenerator = new GeminiTextGenerator({ apiKey });
10638
10662
  }
10639
10663
  const ooc = values["ooc-file"] === void 0 ? null : parseQueriesFile(readFileSync(values["ooc-file"], "utf8"));
@@ -11015,7 +11039,7 @@ async function runContentCli(argv) {
11015
11039
  }
11016
11040
  }
11017
11041
  //#endregion
11018
- //#region ../content/dist/record-DnMnZelb.mjs
11042
+ //#region ../content/dist/record-Cxw0SUG3.mjs
11019
11043
  /**
11020
11044
  * May `surface` publish `doc` at instant `at`? `drafts` is the build's drafts switch,
11021
11045
  * which admits drafts to HUMAN surfaces only.
@@ -11061,6 +11085,7 @@ const lockSchema = z.object({
11061
11085
  drafts: z.enum(["hidden", "shown"]),
11062
11086
  instance_sha256: hex64,
11063
11087
  policy_sha256: hex64,
11088
+ people_sha256: hex64,
11064
11089
  ledger_sha256: hex64,
11065
11090
  ledger_entries: z.array(z.object({
11066
11091
  id: z.string().min(1),
@@ -11133,6 +11158,7 @@ function buildIdOf(inputs) {
11133
11158
  indexes: [...inputs.indexes].map((i) => [i.path, i.sha256]).sort((a, b) => compare(a[0] ?? "", b[0] ?? "")),
11134
11159
  instance_sha256: inputs.instance_sha256,
11135
11160
  policy_sha256: inputs.policy_sha256,
11161
+ people_sha256: inputs.people_sha256,
11136
11162
  ledger_sha256: inputs.ledger_sha256,
11137
11163
  ksor_version: inputs.ksor_version,
11138
11164
  drafts: inputs.drafts
@@ -11179,6 +11205,7 @@ function composeLock(input) {
11179
11205
  }));
11180
11206
  const instance_sha256 = sha256Hex(input.instanceText);
11181
11207
  const policy_sha256 = sha256Hex(input.policyText);
11208
+ const people_sha256 = sha256Hex(input.peopleText ?? "");
11182
11209
  const ledger_sha256 = sha256Hex(input.ledgerText ?? "");
11183
11210
  return {
11184
11211
  format: 1,
@@ -11189,6 +11216,7 @@ function composeLock(input) {
11189
11216
  indexes,
11190
11217
  instance_sha256,
11191
11218
  policy_sha256,
11219
+ people_sha256,
11192
11220
  ledger_sha256,
11193
11221
  ksor_version: input.ksorVersion,
11194
11222
  drafts: input.drafts
@@ -11201,6 +11229,7 @@ function composeLock(input) {
11201
11229
  drafts: input.drafts,
11202
11230
  instance_sha256,
11203
11231
  policy_sha256,
11232
+ people_sha256,
11204
11233
  ledger_sha256,
11205
11234
  ledger_entries: [...input.ledgerEntries].sort((a, b) => compare(a.id, b.id)),
11206
11235
  audiences: {
@@ -11363,6 +11392,7 @@ const INPUTS = [
11363
11392
  "knowledge",
11364
11393
  "instance.md",
11365
11394
  ".ksor/governance.yaml",
11395
+ ".ksor/people.yaml",
11366
11396
  ".ksor/takedowns.yaml"
11367
11397
  ];
11368
11398
  /**
@@ -11373,7 +11403,11 @@ const INPUTS = [
11373
11403
  * clone is invisible to every surface, so the build says so by name.
11374
11404
  */
11375
11405
  function ignoredGovernance(root) {
11376
- const paths = [".ksor/governance.yaml", ".ksor/takedowns.yaml"].filter((rel) => existsSync(join(root, rel)));
11406
+ const paths = [
11407
+ ".ksor/governance.yaml",
11408
+ ".ksor/people.yaml",
11409
+ ".ksor/takedowns.yaml"
11410
+ ].filter((rel) => existsSync(join(root, rel)));
11377
11411
  if (paths.length === 0) return [];
11378
11412
  const out = git(root, [
11379
11413
  "check-ignore",
@@ -11563,8 +11597,8 @@ function runBuild(args, cwd, io, options) {
11563
11597
  const facts = gitFacts(root);
11564
11598
  if (facts.repository && facts.historicLedger === null && !parsed.allowUnverifiable) return refuse$1(io, "ksor-ledger-unverifiable", facts.historyUnreadable === "shallow" ? "this is a shallow clone: the takedown ledger is append-only, and without history a deleted entry cannot be told from one that never existed" : "git could not read the takedown ledger's history (`git log -- .ksor/takedowns.yaml` failed, and this is not a shallow clone): the ledger is append-only, and without history a deleted entry cannot be told from one that never existed", facts.historyUnreadable === "shallow" ? "fetch full history (`git fetch --unshallow`; in CI, `fetch-depth: 0`), or pass --allow-unverifiable-ledger to build anyway" : "check that `git log` works in this checkout, or pass --allow-unverifiable-ledger to build anyway");
11565
11599
  const ignored = facts.repository ? ignoredGovernance(root) : [];
11566
- if (ignored.length > 0) return refuse$1(io, "ksor-governance-ignored", `git ignores ${ignored.join(" and ")}, so ${ignored.length === 1 ? "it is" : "they are"} in no commit — the policy and the takedown ledger ARE the record, and a clone (your CI, your deploy) would build without ${ignored.length === 1 ? "it" : "them"}`, "un-ignore them in .gitignore — the directory form `.ksor/` cannot be negated, so use `.ksor/*` plus `!.ksor/governance.yaml` and `!.ksor/takedowns.yaml` — then commit them (`ksor migrate` offers that edit)");
11567
- if (parsed.strict && facts.dirty) return refuse$1(io, "ksor-build-dirty", facts.repository ? "an input (knowledge/, instance.md, .ksor/governance.yaml, .ksor/takedowns.yaml) differs from its last commit, and --strict stamps only committed content" : "the record is not in a git repository, so no input is committed", "commit the inputs and rebuild, or drop --strict to stamp a dirty build (the lock says `dirty: true`)");
11600
+ if (ignored.length > 0) return refuse$1(io, "ksor-governance-ignored", `git ignores ${ignored.join(" and ")}, so ${ignored.length === 1 ? "it is" : "they are"} in no commit — the policy and the takedown ledger ARE the record, and a clone (your CI, your deploy) would build without ${ignored.length === 1 ? "it" : "them"}`, "un-ignore them in .gitignore — the directory form `.ksor/` cannot be negated, so use `.ksor/*` plus `!.ksor/governance.yaml`, `!.ksor/people.yaml` and `!.ksor/takedowns.yaml` — then commit them (`ksor migrate` offers that edit)");
11601
+ if (parsed.strict && facts.dirty) return refuse$1(io, "ksor-build-dirty", facts.repository ? "an input (knowledge/, instance.md, .ksor/governance.yaml, .ksor/people.yaml, .ksor/takedowns.yaml) differs from its last commit, and --strict stamps only committed content" : "the record is not in a git repository, so no input is committed", "commit the inputs and rebuild, or drop --strict to stamp a dirty build (the lock says `dirty: true`)");
11568
11602
  const baselines = [];
11569
11603
  if (facts.historicLedger !== null) baselines.push({
11570
11604
  source: "git history",
@@ -11605,6 +11639,7 @@ function runBuild(args, cwd, io, options) {
11605
11639
  drafts: options.drafts,
11606
11640
  instanceText: record.files.get("instance.md") ?? "",
11607
11641
  policyText: record.files.get(".ksor/governance.yaml") ?? "",
11642
+ peopleText: record.files.get(".ksor/people.yaml") ?? null,
11608
11643
  ledgerText,
11609
11644
  ledgerEntries: result.ledgerEntries,
11610
11645
  audiences: result.policy?.audiences ?? [],
@@ -13291,11 +13326,13 @@ const BARE_DOTKSOR_PATTERNS = /* @__PURE__ */ new Set([
13291
13326
  ]);
13292
13327
  const GOVERNANCE_IGNORE_BLOCK = [
13293
13328
  "# ksor's working directory — build output and scratch, never the record.",
13294
- "# The two governance files inside it ARE the record (the policy and the",
13295
- "# takedown ledger) and are un-ignored by name: the directory form `.ksor/`",
13296
- "# cannot be negated, so the glob is `.ksor/*`.",
13329
+ "# The governance files inside it ARE the record (the policy, the takedown",
13330
+ "# ledger, and the phone book the site publishes names from) and are",
13331
+ "# un-ignored by name: the directory form `.ksor/` cannot be negated, so the",
13332
+ "# glob is `.ksor/*`.",
13297
13333
  ".ksor/*",
13298
13334
  "!.ksor/governance.yaml",
13335
+ "!.ksor/people.yaml",
13299
13336
  "!.ksor/takedowns.yaml"
13300
13337
  ];
13301
13338
  /**
@@ -13932,7 +13969,9 @@ serves nothing. Runs in this process and holds it; SIGTERM/SIGINT drains.
13932
13969
  Configured by environment — .env beside the record is read automatically:
13933
13970
 
13934
13971
  <database.dsn_env> the Postgres DSN, under the NAME instance.md gives
13935
- GEMINI_API_KEY iff the instance's embedding provider needs a key
13972
+ <provider key> iff the instance's embedding provider needs one:
13973
+ GEMINI_API_KEY for gemini, OPENAI_API_KEY for openai.
13974
+ The refusal names the variable your record needs
13936
13975
  KSOR_AUTH disabled-local (loopback dev) | disabled-public.
13937
13976
  Serve REFUSES to boot with neither this nor a
13938
13977
  configured SSO door — never open by accident