@panaversity/ksor 0.0.53 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +328 -0
- package/README.md +30 -0
- package/dist/checker/check-main.mjs +5 -2
- package/dist/cli.mjs +319 -60
- package/dist/{gateway-api-Ck1l_Sgs-B-_wMYLB.mjs → gateway-api-C0vL3oOK-D24n786A.mjs} +221 -20
- package/dist/gateway.d.mts +2 -2
- package/dist/gateway.mjs +1 -1
- package/docs/deploying.md +48 -9
- package/docs/ingesting.md +30 -8
- 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 +42 -5
- package/templates/scaffold/README.md +79 -13
- package/templates/scaffold/env.example +6 -1
- package/templates/scaffold/gitignore +4 -3
- package/templates/scaffold/mcp.json +13 -0
- 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
|
@@ -171,7 +171,50 @@ That is the whole required config. `embedding:` defaults to Gemini at 1536
|
|
|
171
171
|
dimensions, and `retrieval:` is written for you by step 3's `calibrate`. Change
|
|
172
172
|
the variable name here only if you want a different one.
|
|
173
173
|
|
|
174
|
-
### 2.
|
|
174
|
+
### 2. Get a database — your agent can do this one
|
|
175
|
+
|
|
176
|
+
`.mcp.json` at the repo root declares the MCP servers this project may reach.
|
|
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:
|
|
197
|
+
|
|
198
|
+
> Using the Neon MCP server, create a project called `<your-record>` and enable
|
|
199
|
+
> the pgvector extension on it. Then create a branch called `dev`, and save that
|
|
200
|
+
> branch's connection string to `.env` as `KSOR_DB_URL`. Never print my API key.
|
|
201
|
+
> Show me the plan before you run anything.
|
|
202
|
+
|
|
203
|
+
Prefer the OAuth flow. `.mcp.json` is committed and carries no secret; an API
|
|
204
|
+
key pasted into it would be.
|
|
205
|
+
|
|
206
|
+
Any Postgres with pgvector works — Neon is the path that has an MCP server, not
|
|
207
|
+
a requirement. Locally:
|
|
208
|
+
`docker run -e POSTGRES_PASSWORD=x -p 5432:5432 pgvector/pgvector:pg17`.
|
|
209
|
+
|
|
210
|
+
**`GEMINI_API_KEY` is the one step no agent can do for you** — no vendor mints
|
|
211
|
+
an API key over a protocol. Get it from
|
|
212
|
+
[aistudio.google.com/apikey](https://aistudio.google.com/apikey) and paste it.
|
|
213
|
+
**The free tier costs nothing and is enough**: embedding input on
|
|
214
|
+
`gemini-embedding-001` is free of charge, and a first corpus is a few thousand
|
|
215
|
+
tokens. This is a signup, not a bill.
|
|
216
|
+
|
|
217
|
+
### 3. Fill in the environment
|
|
175
218
|
|
|
176
219
|
```sh
|
|
177
220
|
cp .env.example .env
|
|
@@ -191,7 +234,7 @@ refusal tells you to _export_ a variable, putting it in `.env` is the same
|
|
|
191
234
|
thing. `KSOR_AUTH=disabled-local` is required for a local run: serve refuses to
|
|
192
235
|
boot unauthenticated on purpose, so a server is never open by accident.
|
|
193
236
|
|
|
194
|
-
###
|
|
237
|
+
### 4. Bring it up
|
|
195
238
|
|
|
196
239
|
```sh
|
|
197
240
|
pnpm provision # once: apply the schema, authorize ingest
|
|
@@ -230,17 +273,14 @@ separately](#the-agent-surface-deploys-separately). Any other operation is
|
|
|
230
273
|
### Test the door with an actual agent
|
|
231
274
|
|
|
232
275
|
The MCP door is meant to be read by agents, so check it with one rather than
|
|
233
|
-
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:
|
|
234
278
|
|
|
235
279
|
```json
|
|
236
|
-
{
|
|
237
|
-
"mcpServers": {
|
|
238
280
|
"test-record": {
|
|
239
281
|
"type": "http",
|
|
240
282
|
"url": "http://127.0.0.1:8080/mcp"
|
|
241
283
|
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
284
|
```
|
|
245
285
|
|
|
246
286
|
**If you skipped `calibrate`, expect answers where this test wants refusals** —
|
|
@@ -264,7 +304,9 @@ Question 2 is the one that matters. Anything can answer questions it has the
|
|
|
264
304
|
text for; refusing a plausible near-miss is the property that makes a system of
|
|
265
305
|
record worth trusting, and it is the one that breaks quietly.
|
|
266
306
|
|
|
267
|
-
|
|
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.
|
|
268
310
|
|
|
269
311
|
---
|
|
270
312
|
|
|
@@ -273,7 +315,15 @@ Delete `.mcp.json`, or keep it — it holds no secret.
|
|
|
273
315
|
Both surfaces on one domain, in about ten minutes:
|
|
274
316
|
|
|
275
317
|
1. **Push the repository to GitHub.**
|
|
276
|
-
2. **Import it in Vercel**, then
|
|
318
|
+
2. **Import it in Vercel**, then set **Framework Preset** to **`Services`** and
|
|
319
|
+
**Root Directory** to `./`. The preset is not cosmetic: Vercel's own guide
|
|
320
|
+
says a project builds as services only when the preset is `Services` AND
|
|
321
|
+
`vercel.json` carries a `services` key, and that "if either is missing,
|
|
322
|
+
Vercel falls back to its default framework detection and ignores your
|
|
323
|
+
services configuration" — which is the silent 404 below. No file in this
|
|
324
|
+
repository can set it for you.
|
|
325
|
+
|
|
326
|
+
As for Root Directory: Vercel
|
|
277
327
|
auto-fills it with `system/site`, because that is where it finds a framework
|
|
278
328
|
— and the build then reads `system/site/vercel.json`, which does not exist,
|
|
279
329
|
and fails with `Project framework is set to "services", but no services are
|
|
@@ -282,6 +332,19 @@ declared`. The services ARE declared, in `vercel.json` at the repo root,
|
|
|
282
332
|
container from the root `Dockerfile`.
|
|
283
333
|
3. **Set three environment variables** in Vercel: `KSOR_DB_URL`,
|
|
284
334
|
`GEMINI_API_KEY`, and `KSOR_AUTH=disabled-public`.
|
|
335
|
+
4. **Check it actually serves**, before you tell anyone the URL. A Ready
|
|
336
|
+
deployment that answers 404 everywhere looks identical to a good one from
|
|
337
|
+
the dashboard:
|
|
338
|
+
|
|
339
|
+
```sh
|
|
340
|
+
B=https://your-record.vercel.app
|
|
341
|
+
curl -o /dev/null -w '%{http_code}\n' "$B/" # expect 200
|
|
342
|
+
curl -o /dev/null -w '%{http_code}\n' "$B/llms.txt" # expect 200
|
|
343
|
+
curl -sI "$B/mcp" | head -1 # expect 405
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`/mcp` answering 405 is the door refusing a GET — that is how you know it is
|
|
347
|
+
routed at all, and a 404 there means the `services` block was ignored.
|
|
285
348
|
|
|
286
349
|
Three things catch people here. Two are the system being deliberate; the first
|
|
287
350
|
is not, and it is the one that fails without saying so:
|
|
@@ -289,10 +352,13 @@ is not, and it is the one that fails without saying so:
|
|
|
289
352
|
- **A deployment can report Ready and serve nothing.** The build succeeds,
|
|
290
353
|
Vercel collects nothing, and the deployment takes your domain and answers
|
|
291
354
|
`404: NOT_FOUND` everywhere — with one build-log line as the only signal:
|
|
292
|
-
`WARNING! Build output contains no "functions" or "static" directory`.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
355
|
+
`WARNING! Build output contains no "functions" or "static" directory`. That
|
|
356
|
+
warning is the FALLBACK collector finding nothing, which is what step 2's
|
|
357
|
+
preset exists to prevent — check it first. One measurement of ours disagrees
|
|
358
|
+
with the vendor's rule and is recorded in
|
|
359
|
+
`node_modules/@panaversity/ksor/docs/deploying.md`; the honest state is that
|
|
360
|
+
the preset is necessary by the vendor's documentation and has once appeared
|
|
361
|
+
not to be. If you hit this, the fallback is the
|
|
296
362
|
classic-keys form in `node_modules/@panaversity/ksor/docs/deploying.md` — read
|
|
297
363
|
it there rather than guessing, because it **moves the door off your domain**
|
|
298
364
|
and `KSOR_MCP_RESOURCE_URL` and your SSO API Identifier both have to move with
|
|
@@ -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
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"Neon": {
|
|
4
|
+
"type": "http",
|
|
5
|
+
"url": "https://mcp.neon.tech/mcp"
|
|
6
|
+
},
|
|
7
|
+
"agentfactory-system-of-record": {
|
|
8
|
+
"type": "http",
|
|
9
|
+
"url": "https://sor.panaversity.org/mcp",
|
|
10
|
+
"oauth": { "clientId": "zia-tutor-ai", "callbackPort": 3118 }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -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 },
|