@panaversity/ksor 0.0.54 → 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 +179 -0
- package/dist/checker/check-main.mjs +5 -2
- package/dist/cli.mjs +56 -17
- package/dist/{gateway-api-uhx2l1kC-C2BAxISt.mjs → gateway-api-C0vL3oOK-D24n786A.mjs} +25 -4
- package/dist/gateway.d.mts +2 -2
- package/dist/gateway.mjs +1 -1
- package/docs/deploying.md +7 -1
- package/docs/ingesting.md +10 -5
- package/package.json +1 -1
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +3 -0
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +3 -0
- package/templates/scaffold/AGENTS.md +9 -1
- package/templates/scaffold/README.md +25 -7
- package/templates/scaffold/env.example +6 -1
- package/templates/scaffold/gitignore +4 -3
- package/templates/scaffold/system/site/lib/lock.ts +8 -1
- package/templates/scaffold/system/site/lib/people-rule.ts +56 -0
- package/templates/scaffold/system/site/lib/people.ts +5 -24
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +2 -0
- package/templates/scaffold/system/site/record/load.ts +11 -1
- package/templates/scaffold/system/site/record/lock.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,184 @@
|
|
|
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
|
+
|
|
3
182
|
## 0.0.54
|
|
4
183
|
|
|
5
184
|
### 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
6308
|
-
|
|
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-
|
|
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 = [
|
|
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
|
|
13295
|
-
"#
|
|
13296
|
-
"# cannot be negated, so the
|
|
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
|
-
|
|
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
|
|
@@ -6,7 +6,7 @@ import { z, z as z$1 } from "zod";
|
|
|
6
6
|
import path, { join } from "node:path";
|
|
7
7
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
8
8
|
import pg from "pg";
|
|
9
|
-
//#region ../content-gateway/dist/gateway-api-
|
|
9
|
+
//#region ../content-gateway/dist/gateway-api-C0vL3oOK.mjs
|
|
10
10
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
11
11
|
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
12
12
|
/**
|
|
@@ -9159,6 +9159,14 @@ function isRetryable(exc) {
|
|
|
9159
9159
|
* project stays rate-limited on the next second, so a search degrades to
|
|
9160
9160
|
* keyword-only now rather than stalling a reader behind backoff.
|
|
9161
9161
|
*/
|
|
9162
|
+
/**
|
|
9163
|
+
* An ACCOUNT-level failure: no amount of waiting and no other passage changes
|
|
9164
|
+
* it. The drain must abort on this rather than quarantine, because the chunk
|
|
9165
|
+
* it happened to be holding is not what is wrong — see `ingest/worker.ts`.
|
|
9166
|
+
*/
|
|
9167
|
+
function isFatal(exc) {
|
|
9168
|
+
return exc instanceof OpenAiHttpError && exc.kind === "insufficient_quota";
|
|
9169
|
+
}
|
|
9162
9170
|
function isRetryableQuery(exc) {
|
|
9163
9171
|
if (isTransportBlip(exc)) return true;
|
|
9164
9172
|
const status = httpStatusOf(exc);
|
|
@@ -9209,6 +9217,9 @@ var OpenAiEmbeddingProvider = class {
|
|
|
9209
9217
|
isRetryableQuery(exc) {
|
|
9210
9218
|
return isRetryableQuery(exc);
|
|
9211
9219
|
}
|
|
9220
|
+
isFatal(exc) {
|
|
9221
|
+
return isFatal(exc);
|
|
9222
|
+
}
|
|
9212
9223
|
};
|
|
9213
9224
|
/**
|
|
9214
9225
|
* The embedding-provider registry — a plain object, NOT any discovery
|
|
@@ -9235,10 +9246,20 @@ var OpenAiEmbeddingProvider = class {
|
|
|
9235
9246
|
*/
|
|
9236
9247
|
var MissingProviderKeyError = class extends Error {
|
|
9237
9248
|
providerName;
|
|
9238
|
-
|
|
9239
|
-
|
|
9249
|
+
keyEnv;
|
|
9250
|
+
/**
|
|
9251
|
+
* `keyEnv` is not decoration. The message named the PROVIDER and nothing
|
|
9252
|
+
* else, so an operator whose `ksor serve` exited 3 on an OpenAI record was
|
|
9253
|
+
* told "provider openai needs an API key" and left to guess which variable —
|
|
9254
|
+
* while `ksor serve --help`, `env.example` and `docs/deploying.md` all named
|
|
9255
|
+
* `GEMINI_API_KEY`, which the door does not read (review, 2026-09-01). The
|
|
9256
|
+
* registry row already held the answer; this is it reaching the operator.
|
|
9257
|
+
*/
|
|
9258
|
+
constructor(providerName, keyEnv = null) {
|
|
9259
|
+
super(`embedding provider ${JSON.stringify(providerName)} needs an API key and none was supplied` + (keyEnv === null ? "" : ` — set ${keyEnv}`));
|
|
9240
9260
|
this.name = "MissingProviderKeyError";
|
|
9241
9261
|
this.providerName = providerName;
|
|
9262
|
+
this.keyEnv = keyEnv;
|
|
9242
9263
|
}
|
|
9243
9264
|
};
|
|
9244
9265
|
const PROVIDERS = {
|
|
@@ -9294,7 +9315,7 @@ function providerKeyEnv(name) {
|
|
|
9294
9315
|
*/
|
|
9295
9316
|
function buildShippedProvider(name, opts) {
|
|
9296
9317
|
const entry = entryFor(name);
|
|
9297
|
-
if (entry.needsApiKey && !opts.apiKey) throw new MissingProviderKeyError(name);
|
|
9318
|
+
if (entry.needsApiKey && !opts.apiKey) throw new MissingProviderKeyError(name, entry.keyEnv);
|
|
9298
9319
|
return entry.build({
|
|
9299
9320
|
apiKey: opts.apiKey ?? "",
|
|
9300
9321
|
modelId: opts.modelId ?? "gemini-embedding-001",
|
package/dist/gateway.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CallToolResult, McpServer as McpServer$1, StandardSchemaWithJSON } from "@modelcontextprotocol/server";
|
|
2
2
|
import { z as z$1 } from "zod";
|
|
3
3
|
import pg from "pg";
|
|
4
|
-
//#region ../content-gateway/dist/gateway-api-
|
|
4
|
+
//#region ../content-gateway/dist/gateway-api-CEnK8Bc8.d.mts
|
|
5
5
|
//#region src/instructions.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Has the owner said what this record is FOR yet?
|
|
@@ -16,7 +16,7 @@ import pg from "pg";
|
|
|
16
16
|
declare function recordIsUndescribed(authored: string): boolean;
|
|
17
17
|
declare function composeInstructions(authored: string): string;
|
|
18
18
|
//#endregion
|
|
19
|
-
//#region ../content/dist/index-
|
|
19
|
+
//#region ../content/dist/index-D-xEl8mz.d.mts
|
|
20
20
|
declare const TRUST_TIERS: readonly ["unverified", "machine-confirmed", "human-reviewed"];
|
|
21
21
|
type TrustTier = (typeof TRUST_TIERS)[number];
|
|
22
22
|
//#endregion
|
package/dist/gateway.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { D as outlineHandler, F as recordIsUndescribed, G as z$1, L as searchHandler, P as readHandler, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, h as TRUST_TIERS, i as FLOOR, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, s as MAX_SEARCH_K, u as OUTLINE_OUTPUT, x as composeInstructions } from "./gateway-api-
|
|
1
|
+
import { D as outlineHandler, F as recordIsUndescribed, G as z$1, L as searchHandler, P as readHandler, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, h as TRUST_TIERS, i as FLOOR, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, s as MAX_SEARCH_K, u as OUTLINE_OUTPUT, x as composeInstructions } from "./gateway-api-C0vL3oOK-D24n786A.mjs";
|
|
2
2
|
export { FLOOR, MAX_OUTLINE_LIMIT, MAX_SEARCH_K, McpServer$1 as McpServer, OUTLINE_OUTPUT, READ_ONLY, READ_OUTPUT, SEARCH_OUTPUT, TRUST_TIERS, composeInstructions, outlineHandler, readHandler, recordIsUndescribed, searchHandler, z$1 as z };
|
package/docs/deploying.md
CHANGED
|
@@ -286,9 +286,15 @@ are doing it wrong" — and only the first tier is true.
|
|
|
286
286
|
| variable | why |
|
|
287
287
|
| ----------------------------------------- | ---------------------------------------------------- |
|
|
288
288
|
| `KSOR_DB_URL` | the record's Postgres store |
|
|
289
|
-
|
|
|
289
|
+
| the provider key | embeds the incoming query, so retrieval works at all |
|
|
290
290
|
| `KSOR_AUTH`, **or** a configured SSO door | see below |
|
|
291
291
|
|
|
292
|
+
The provider key is whichever variable `embedding.provider` in `instance.md`
|
|
293
|
+
names — `GEMINI_API_KEY` for `gemini` (the default), `OPENAI_API_KEY` for
|
|
294
|
+
`openai`. A record reads exactly one, and the boot refusal names the one it
|
|
295
|
+
wanted: `embedding provider "openai" needs an API key and none was supplied —
|
|
296
|
+
set OPENAI_API_KEY`.
|
|
297
|
+
|
|
292
298
|
`KSOR_AUTH` takes one of two values, and the value IS the decision:
|
|
293
299
|
|
|
294
300
|
```sh
|
package/docs/ingesting.md
CHANGED
|
@@ -197,11 +197,16 @@ measure until the corpus is in there.
|
|
|
197
197
|
pnpm exec ksor calibrate --instance instance.md
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
**
|
|
201
|
-
|
|
202
|
-
and
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
**The synthesized door needs `GEMINI_API_KEY`, whatever your embedding provider
|
|
201
|
+
is.** The command above writes one probe question per sampled passage with an
|
|
202
|
+
LLM, and question synthesis is Gemini-only today — so a record on
|
|
203
|
+
`embedding.provider: openai` embeds with `OPENAI_API_KEY` and would still be
|
|
204
|
+
refused here for a Google key. That is a real gap, stated rather than papered
|
|
205
|
+
over; the zero-LLM door below avoids it entirely and is the better choice on a
|
|
206
|
+
free-tier key anyway, because a free key allows only a few generations a minute
|
|
207
|
+
and a bigger corpus makes that worse, not better.
|
|
208
|
+
|
|
209
|
+
Write your own in-corpus questions, one per line, and pass them:
|
|
205
210
|
|
|
206
211
|
```sh
|
|
207
212
|
pnpm exec ksor calibrate --instance instance.md --queries-file questions.txt
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panaversity/ksor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
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",
|
|
@@ -11971,6 +11971,7 @@ function changedFields(before, after) {
|
|
|
11971
11971
|
const CONTROL_FILES = [
|
|
11972
11972
|
"instance.md",
|
|
11973
11973
|
".ksor/governance.yaml",
|
|
11974
|
+
".ksor/people.yaml",
|
|
11974
11975
|
".ksor/takedowns.yaml"
|
|
11975
11976
|
];
|
|
11976
11977
|
/** Files the operating system writes behind the author's back: ignored, never reported. */
|
|
@@ -13551,6 +13552,7 @@ const lockSchema = object({
|
|
|
13551
13552
|
drafts: _enum(["hidden", "shown"]),
|
|
13552
13553
|
instance_sha256: hex64,
|
|
13553
13554
|
policy_sha256: hex64,
|
|
13555
|
+
people_sha256: hex64,
|
|
13554
13556
|
ledger_sha256: hex64,
|
|
13555
13557
|
ledger_entries: array(object({
|
|
13556
13558
|
id: string().min(1),
|
|
@@ -13745,6 +13747,7 @@ const INPUTS = [
|
|
|
13745
13747
|
"knowledge",
|
|
13746
13748
|
"instance.md",
|
|
13747
13749
|
".ksor/governance.yaml",
|
|
13750
|
+
".ksor/people.yaml",
|
|
13748
13751
|
".ksor/takedowns.yaml"
|
|
13749
13752
|
];
|
|
13750
13753
|
function gitFacts(root) {
|
|
@@ -11971,6 +11971,7 @@ function changedFields(before, after) {
|
|
|
11971
11971
|
const CONTROL_FILES = [
|
|
11972
11972
|
"instance.md",
|
|
11973
11973
|
".ksor/governance.yaml",
|
|
11974
|
+
".ksor/people.yaml",
|
|
11974
11975
|
".ksor/takedowns.yaml"
|
|
11975
11976
|
];
|
|
11976
11977
|
/** Files the operating system writes behind the author's back: ignored, never reported. */
|
|
@@ -13551,6 +13552,7 @@ const lockSchema = object({
|
|
|
13551
13552
|
drafts: _enum(["hidden", "shown"]),
|
|
13552
13553
|
instance_sha256: hex64,
|
|
13553
13554
|
policy_sha256: hex64,
|
|
13555
|
+
people_sha256: hex64,
|
|
13554
13556
|
ledger_sha256: hex64,
|
|
13555
13557
|
ledger_entries: array(object({
|
|
13556
13558
|
id: string().min(1),
|
|
@@ -13745,6 +13747,7 @@ const INPUTS = [
|
|
|
13745
13747
|
"knowledge",
|
|
13746
13748
|
"instance.md",
|
|
13747
13749
|
".ksor/governance.yaml",
|
|
13750
|
+
".ksor/people.yaml",
|
|
13748
13751
|
".ksor/takedowns.yaml"
|
|
13749
13752
|
];
|
|
13750
13753
|
function gitFacts(root) {
|
|
@@ -118,7 +118,15 @@ text-embedding-3-small`, `dim: 1536`, key in `OPENAI_API_KEY` — and each
|
|
|
118
118
|
Turning it on is step 4, AFTER the record is serving.
|
|
119
119
|
|
|
120
120
|
2. **Get the database — your agent can do this one.** `.mcp.json` at the repo
|
|
121
|
-
root declares the MCP servers this project may reach
|
|
121
|
+
root declares the MCP servers this project may reach. It ships with two:
|
|
122
|
+
`Neon`, which provisions the Postgres this step needs, and
|
|
123
|
+
`agentfactory-system-of-record`, a read-only KSoR record Panaversity operates
|
|
124
|
+
as an example of the surface being built here — not this project's record,
|
|
125
|
+
and not needed by anything below. Either can be deleted; the file is the
|
|
126
|
+
adopter's.
|
|
127
|
+
|
|
128
|
+
The Neon server acts on the whole Neon ACCOUNT, not on one database: it can
|
|
129
|
+
create and delete projects and branches. Show the plan before running it.
|
|
122
130
|
With it connected, ask:
|
|
123
131
|
|
|
124
132
|
> Using the Neon MCP server, create a project called `<your-record>` and
|
|
@@ -174,7 +174,26 @@ the variable name here only if you want a different one.
|
|
|
174
174
|
### 2. Get a database — your agent can do this one
|
|
175
175
|
|
|
176
176
|
`.mcp.json` at the repo root declares the MCP servers this project may reach.
|
|
177
|
-
|
|
177
|
+
It ships with two, and both are yours to keep or delete — it is your file:
|
|
178
|
+
|
|
179
|
+
| server | what it is |
|
|
180
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
181
|
+
| `Neon` | Neon's own hosted MCP server, for provisioning the Postgres the agent surface needs (step 2) |
|
|
182
|
+
| `agentfactory-system-of-record` | a KSoR record Panaversity operates, served over MCP — an example of the surface you are building |
|
|
183
|
+
|
|
184
|
+
The second is **not** your record and is not needed to run this project. It is
|
|
185
|
+
read-only and it answers about the Agent Factory curriculum, not about your
|
|
186
|
+
knowledge. Delete the entry if you would rather your agent not have it; nothing
|
|
187
|
+
here depends on it.
|
|
188
|
+
|
|
189
|
+
**Before you connect Neon, know what you are granting.** The Neon MCP server
|
|
190
|
+
acts on your Neon *account*, not on one database: an agent holding it can create
|
|
191
|
+
and delete projects and branches. Point it at an account you are willing to let
|
|
192
|
+
an agent change, review the plan it shows you before approving, and read Neon's
|
|
193
|
+
own documentation on the server's scopes and permissions rather than taking this
|
|
194
|
+
paragraph as the whole of it.
|
|
195
|
+
|
|
196
|
+
With it connected, ask your coding agent:
|
|
178
197
|
|
|
179
198
|
> Using the Neon MCP server, create a project called `<your-record>` and enable
|
|
180
199
|
> the pgvector extension on it. Then create a branch called `dev`, and save that
|
|
@@ -254,17 +273,14 @@ separately](#the-agent-surface-deploys-separately). Any other operation is
|
|
|
254
273
|
### Test the door with an actual agent
|
|
255
274
|
|
|
256
275
|
The MCP door is meant to be read by agents, so check it with one rather than
|
|
257
|
-
with `curl`. With `pnpm serve` running,
|
|
276
|
+
with `curl`. With `pnpm serve` running, **add** an entry to the `.mcp.json` you
|
|
277
|
+
already have — alongside `Neon`, not in place of it:
|
|
258
278
|
|
|
259
279
|
```json
|
|
260
|
-
{
|
|
261
|
-
"mcpServers": {
|
|
262
280
|
"test-record": {
|
|
263
281
|
"type": "http",
|
|
264
282
|
"url": "http://127.0.0.1:8080/mcp"
|
|
265
283
|
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
284
|
```
|
|
269
285
|
|
|
270
286
|
**If you skipped `calibrate`, expect answers where this test wants refusals** —
|
|
@@ -288,7 +304,9 @@ Question 2 is the one that matters. Anything can answer questions it has the
|
|
|
288
304
|
text for; refusing a plausible near-miss is the property that makes a system of
|
|
289
305
|
record worth trusting, and it is the one that breaks quietly.
|
|
290
306
|
|
|
291
|
-
|
|
307
|
+
Keep the `test-record` entry or remove it — it points at loopback and holds no
|
|
308
|
+
secret either way. Do not delete `.mcp.json` itself: it is where `Neon` is
|
|
309
|
+
declared, and step 2 above needs it.
|
|
292
310
|
|
|
293
311
|
---
|
|
294
312
|
|
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
# explicitly instead of relying on a driver default that is due to change.
|
|
11
11
|
KSOR_DB_URL=postgresql://user:password@host:5432/dbname
|
|
12
12
|
|
|
13
|
-
# The embedding provider key
|
|
13
|
+
# The embedding provider key — ONE of these, whichever `embedding.provider` in
|
|
14
|
+
# instance.md names. It defaults to gemini, so that is the one uncommented; on
|
|
15
|
+
# `provider: openai`, comment this out and set OPENAI_API_KEY instead. A record
|
|
16
|
+
# reads exactly one of them, and `ksor serve` names the one it wanted when it
|
|
17
|
+
# is missing.
|
|
14
18
|
GEMINI_API_KEY=
|
|
19
|
+
# OPENAI_API_KEY=
|
|
15
20
|
|
|
16
21
|
# ── Who may ask ─────────────────────────────────────────────────────────────
|
|
17
22
|
# ONE variable, and its VALUE is the decision. `ksor serve` refuses to boot
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# ksor's working directory — build output and scratch, never the record.
|
|
2
|
-
# The
|
|
3
|
-
#
|
|
4
|
-
# cannot be negated, so the glob is
|
|
2
|
+
# The governance files inside it ARE the record (the policy, the takedown
|
|
3
|
+
# ledger, and the phone book the site publishes names from) and are un-ignored
|
|
4
|
+
# by name: the directory form `.ksor/` cannot be negated, so the glob is
|
|
5
|
+
# `.ksor/*`.
|
|
5
6
|
.ksor/*
|
|
6
7
|
!.ksor/governance.yaml
|
|
7
8
|
!.ksor/people.yaml
|
|
@@ -73,6 +73,8 @@ export interface ControlTexts {
|
|
|
73
73
|
readonly policy: string;
|
|
74
74
|
/** Null when `.ksor/takedowns.yaml` is not there — an empty ledger. */
|
|
75
75
|
readonly ledger: string | null;
|
|
76
|
+
/** Null when `.ksor/people.yaml` is not there — no natural names declared. */
|
|
77
|
+
readonly people: string | null;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
function sha256Text(text: string): string {
|
|
@@ -139,12 +141,17 @@ export function readLock(
|
|
|
139
141
|
["instance.md", lock.data.instance_sha256, sha256Text(control.instance)],
|
|
140
142
|
[".ksor/governance.yaml", lock.data.policy_sha256, sha256Text(control.policy)],
|
|
141
143
|
[".ksor/takedowns.yaml", lock.data.ledger_sha256, sha256Text(control.ledger ?? "")],
|
|
144
|
+
// The phone book publishes NAMES in place of stored actors, so an edit here
|
|
145
|
+
// changes the approver printed on every page. Left out, the site could
|
|
146
|
+
// publish one approver while the `/md/` twin stamped with the same
|
|
147
|
+
// `build_id` published another (review, 2026-09-01).
|
|
148
|
+
[".ksor/people.yaml", lock.data.people_sha256, sha256Text(control.people ?? "")],
|
|
142
149
|
] as const) {
|
|
143
150
|
if (want === have) continue;
|
|
144
151
|
refuse(
|
|
145
152
|
"ksor-lock-stale",
|
|
146
153
|
`${file} changed since ${LOCK_FILE} was written`,
|
|
147
|
-
"the lock's build_id is a hash over the record AND the
|
|
154
|
+
"the lock's build_id is a hash over the record AND the four files that govern it, so a projection under a control file the lock never saw publishes what nothing checked — a denial lifted by deleting a line would otherwise leave the lock valid",
|
|
148
155
|
"run `ksor build` again and commit the lock with the change; lift a denial with `ksor takedown --revoke <id>`, never by editing the ledger",
|
|
149
156
|
);
|
|
150
157
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a phone book file DECLARES, as a pure function of its text.
|
|
3
|
+
*
|
|
4
|
+
* A LEAF, like `actor-display.ts`: `people.ts` roots itself at the project via
|
|
5
|
+
* `shared.ts`, which reads `instance.md` at module load, so a rule living
|
|
6
|
+
* there could only be exercised by building a whole record on disk first. That
|
|
7
|
+
* is a test nobody writes, and the duplicate-key rule below went four days
|
|
8
|
+
* asserted only by a comment because of it.
|
|
9
|
+
*
|
|
10
|
+
* A map comes back for every malformed shape rather than an error, because a
|
|
11
|
+
* phone book is DECORATION: a record with an unreadable one publishes
|
|
12
|
+
* identifiers, which is exactly what it did before the file existed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parseAllDocuments } from "yaml";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The phone book a file DECLARES, as a pure function of its text.
|
|
19
|
+
*
|
|
20
|
+
* Separated from the read so the rule can be exercised without a record on
|
|
21
|
+
* disk: the loader below is a filesystem act rooted at `projectRoot`, and a
|
|
22
|
+
* test that has to build a whole scaffold to ask "what do two entries for one
|
|
23
|
+
* actor mean?" is a test nobody writes. A map is handed back for every
|
|
24
|
+
* malformed shape rather than an error, because a phone book is decoration:
|
|
25
|
+
* a record with an unreadable one publishes identifiers, which is what it did
|
|
26
|
+
* before the file existed.
|
|
27
|
+
*/
|
|
28
|
+
export function parsePeople(text: string): ReadonlyMap<string, string> {
|
|
29
|
+
try {
|
|
30
|
+
const docs = parseAllDocuments(text.replace(/^\ufeff/, ""), {
|
|
31
|
+
schema: "core",
|
|
32
|
+
uniqueKeys: true,
|
|
33
|
+
logLevel: "silent",
|
|
34
|
+
});
|
|
35
|
+
const doc = docs[0];
|
|
36
|
+
if (doc === undefined) return new Map();
|
|
37
|
+
// `uniqueKeys: true` RECORDS a duplicate rather than refusing one — `toJS()`
|
|
38
|
+
// still resolves last-wins. Reading the option and not the errors is what
|
|
39
|
+
// made the claim above false: two entries for one actor silently published
|
|
40
|
+
// the second person's name on the first person's governance act.
|
|
41
|
+
if (doc.errors.length > 0) return new Map();
|
|
42
|
+
const value: unknown = doc.toJS();
|
|
43
|
+
if (typeof value !== "object" || value === null) return new Map();
|
|
44
|
+
const table = (value as { people?: unknown }).people;
|
|
45
|
+
if (typeof table !== "object" || table === null || Array.isArray(table)) return new Map();
|
|
46
|
+
const out = new Map<string, string>();
|
|
47
|
+
for (const [actor, name] of Object.entries(table as Record<string, unknown>)) {
|
|
48
|
+
// A blank value is an entry someone started and left; printing "" would
|
|
49
|
+
// erase the identifier rather than replace it.
|
|
50
|
+
if (typeof name === "string" && name.trim() !== "") out.set(actor.trim(), name.trim());
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
} catch {
|
|
54
|
+
return new Map();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* `human:mjs` — most of the actors in a real record — had no expressible name
|
|
9
9
|
* at all. It also collided: "Bashir Aziz" and "Bashira Ziz" both derive
|
|
10
10
|
* `bashiraziz`, which would print one person's name on the other's governance
|
|
11
|
-
* act. A map has neither problem, and duplicate
|
|
12
|
-
* rather than
|
|
11
|
+
* act. A map has neither problem, and a duplicate key drops the whole book
|
|
12
|
+
* rather than resolving to whichever came last — `uniqueKeys` only REPORTS the
|
|
13
|
+
* duplicate, so the errors have to be read for that to be true.
|
|
13
14
|
*
|
|
14
15
|
* ONE-WAY. The identifier is what the record stores, cites and checks against
|
|
15
16
|
* the policy; this is only what a page prints. Nothing reads a name back into
|
|
@@ -34,8 +35,7 @@
|
|
|
34
35
|
import { readFileSync } from "node:fs";
|
|
35
36
|
import path from "node:path";
|
|
36
37
|
|
|
37
|
-
import {
|
|
38
|
-
|
|
38
|
+
import { parsePeople } from "./people-rule";
|
|
39
39
|
import { projectRoot } from "./shared";
|
|
40
40
|
|
|
41
41
|
const PEOPLE_YAML = path.join(projectRoot, ".ksor", "people.yaml");
|
|
@@ -48,26 +48,7 @@ function loadPeople(): ReadonlyMap<string, string> {
|
|
|
48
48
|
// Optional: its absence means "no natural names declared".
|
|
49
49
|
return new Map();
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
const docs = parseAllDocuments(text.replace(/^/, ""), {
|
|
53
|
-
schema: "core",
|
|
54
|
-
uniqueKeys: true,
|
|
55
|
-
logLevel: "silent",
|
|
56
|
-
});
|
|
57
|
-
const value: unknown = docs[0]?.toJS();
|
|
58
|
-
if (typeof value !== "object" || value === null) return new Map();
|
|
59
|
-
const table = (value as { people?: unknown }).people;
|
|
60
|
-
if (typeof table !== "object" || table === null || Array.isArray(table)) return new Map();
|
|
61
|
-
const out = new Map<string, string>();
|
|
62
|
-
for (const [actor, name] of Object.entries(table as Record<string, unknown>)) {
|
|
63
|
-
// A blank value is an entry someone started and left; printing "" would
|
|
64
|
-
// erase the identifier rather than replace it.
|
|
65
|
-
if (typeof name === "string" && name.trim() !== "") out.set(actor.trim(), name.trim());
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
} catch {
|
|
69
|
-
return new Map();
|
|
70
|
-
}
|
|
51
|
+
return parsePeople(text);
|
|
71
52
|
}
|
|
72
53
|
|
|
73
54
|
let cached: ReadonlyMap<string, string> | null = null;
|
|
@@ -48,6 +48,7 @@ import type { Refusal } from "../record/refusal";
|
|
|
48
48
|
const KNOWLEDGE = "knowledge/";
|
|
49
49
|
const LEDGER_PATH = ".ksor/takedowns.yaml";
|
|
50
50
|
const POLICY_PATH = ".ksor/governance.yaml";
|
|
51
|
+
const PEOPLE_PATH = ".ksor/people.yaml";
|
|
51
52
|
/**
|
|
52
53
|
* Everything this build may publish, as bytes at bundle-relative paths: the
|
|
53
54
|
* admitted concepts (copied), their companions (copied), ONLY the assets those
|
|
@@ -177,6 +178,7 @@ function planStage(recordDir: string, development: boolean): StagePlan {
|
|
|
177
178
|
instance: record.files.get("instance.md") ?? "",
|
|
178
179
|
policy: record.files.get(POLICY_PATH) ?? "",
|
|
179
180
|
ledger: record.files.get(LEDGER_PATH) ?? null,
|
|
181
|
+
people: record.files.get(PEOPLE_PATH) ?? null,
|
|
180
182
|
},
|
|
181
183
|
{ draftsRequested },
|
|
182
184
|
);
|
|
@@ -11,7 +11,17 @@ import path from "node:path";
|
|
|
11
11
|
import type { RecordFiles } from "./check";
|
|
12
12
|
import type { ScaffoldStructure } from "./hygiene";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// `.ksor/people.yaml` is here because the SITE reads it and publishes what it
|
|
15
|
+
// says — it rewrites the approver, owner and verifier printed on every document
|
|
16
|
+
// page. A file that changes published bytes has to reach `build_id`, or the
|
|
17
|
+
// human surface and the machine surface of one build can disagree about who
|
|
18
|
+
// approved a document with nothing going red (found by review, 2026-09-01).
|
|
19
|
+
const CONTROL_FILES = [
|
|
20
|
+
"instance.md",
|
|
21
|
+
".ksor/governance.yaml",
|
|
22
|
+
".ksor/people.yaml",
|
|
23
|
+
".ksor/takedowns.yaml",
|
|
24
|
+
] as const;
|
|
15
25
|
/** Files the operating system writes behind the author's back: ignored, never reported. */
|
|
16
26
|
const OS_JUNK = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
|
|
17
27
|
|
|
@@ -46,6 +46,7 @@ const lockSchema = z
|
|
|
46
46
|
drafts: z.enum(["hidden", "shown"]),
|
|
47
47
|
instance_sha256: hex64,
|
|
48
48
|
policy_sha256: hex64,
|
|
49
|
+
people_sha256: hex64,
|
|
49
50
|
ledger_sha256: hex64,
|
|
50
51
|
ledger_entries: z.array(z.object({ id: z.string().min(1), digest: hex64 }).strict()),
|
|
51
52
|
audiences: z
|
|
@@ -89,6 +90,8 @@ export interface Lock {
|
|
|
89
90
|
readonly drafts: Drafts;
|
|
90
91
|
readonly instance_sha256: string;
|
|
91
92
|
readonly policy_sha256: string;
|
|
93
|
+
/** The phone book the SITE publishes names from. Empty string when absent. */
|
|
94
|
+
readonly people_sha256: string;
|
|
92
95
|
readonly ledger_sha256: string;
|
|
93
96
|
/** `(id, digest)` per ledger entry, sorted by id — the baseline the next build compares TEXT against. */
|
|
94
97
|
readonly ledger_entries: readonly { readonly id: string; readonly digest: string }[];
|
|
@@ -154,6 +157,7 @@ export interface BuildIdInputs {
|
|
|
154
157
|
readonly indexes: readonly { readonly path: string; readonly sha256: string }[];
|
|
155
158
|
readonly instance_sha256: string;
|
|
156
159
|
readonly policy_sha256: string;
|
|
160
|
+
readonly people_sha256: string;
|
|
157
161
|
readonly ledger_sha256: string;
|
|
158
162
|
readonly ksor_version: string;
|
|
159
163
|
readonly drafts: Drafts;
|
|
@@ -181,6 +185,9 @@ export function buildIdOf(inputs: BuildIdInputs): string {
|
|
|
181
185
|
.sort((a, b) => compare(a[0] ?? "", b[0] ?? "")),
|
|
182
186
|
instance_sha256: inputs.instance_sha256,
|
|
183
187
|
policy_sha256: inputs.policy_sha256,
|
|
188
|
+
// The site prints what this file says in place of the stored actor, so it
|
|
189
|
+
// is published content and belongs in the id like any other.
|
|
190
|
+
people_sha256: inputs.people_sha256,
|
|
184
191
|
ledger_sha256: inputs.ledger_sha256,
|
|
185
192
|
ksor_version: inputs.ksor_version,
|
|
186
193
|
drafts: inputs.drafts,
|
|
@@ -233,6 +240,8 @@ export interface LockInput {
|
|
|
233
240
|
readonly drafts: Drafts;
|
|
234
241
|
readonly instanceText: string;
|
|
235
242
|
readonly policyText: string;
|
|
243
|
+
/** Null when `.ksor/people.yaml` does not exist, which is every record until an owner writes one. */
|
|
244
|
+
readonly peopleText: string | null;
|
|
236
245
|
/** Null when the ledger file does not exist. */
|
|
237
246
|
readonly ledgerText: string | null;
|
|
238
247
|
readonly ledgerEntries: readonly { readonly id: string; readonly digest: string }[];
|
|
@@ -269,6 +278,7 @@ export function composeLock(input: LockInput): Lock {
|
|
|
269
278
|
.map((i) => ({ path: i.path, sha256: sha256Hex(i.text) }));
|
|
270
279
|
const instance_sha256 = sha256Hex(input.instanceText);
|
|
271
280
|
const policy_sha256 = sha256Hex(input.policyText);
|
|
281
|
+
const people_sha256 = sha256Hex(input.peopleText ?? "");
|
|
272
282
|
const ledger_sha256 = sha256Hex(input.ledgerText ?? "");
|
|
273
283
|
return {
|
|
274
284
|
format: LOCK_FORMAT,
|
|
@@ -279,6 +289,7 @@ export function composeLock(input: LockInput): Lock {
|
|
|
279
289
|
indexes,
|
|
280
290
|
instance_sha256,
|
|
281
291
|
policy_sha256,
|
|
292
|
+
people_sha256,
|
|
282
293
|
ledger_sha256,
|
|
283
294
|
ksor_version: input.ksorVersion,
|
|
284
295
|
drafts: input.drafts,
|
|
@@ -291,6 +302,7 @@ export function composeLock(input: LockInput): Lock {
|
|
|
291
302
|
drafts: input.drafts,
|
|
292
303
|
instance_sha256,
|
|
293
304
|
policy_sha256,
|
|
305
|
+
people_sha256,
|
|
294
306
|
ledger_sha256,
|
|
295
307
|
ledger_entries: [...input.ledgerEntries].sort((a, b) => compare(a.id, b.id)),
|
|
296
308
|
audiences: { registry: [...input.audiences].sort(), viewers },
|