@panaversity/ksor 0.0.57 → 0.0.59
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 +205 -0
- package/README.md +19 -15
- package/dist/checker/check-main.mjs +142 -136
- package/dist/cli.mjs +812 -212
- package/dist/{gateway-api-C0vL3oOK-D24n786A.mjs → gateway-api-CEsb-e8z-Ch6oMaq2.mjs} +74 -7
- package/dist/gateway.d.mts +2 -2
- package/dist/gateway.mjs +1 -1
- package/docs/building.md +157 -0
- package/docs/deploying.md +17 -5
- package/docs/index.md +7 -3
- package/docs/ingesting.md +36 -6
- package/docs/tool-surface.md +0 -3
- package/docs/upgrading.md +16 -2
- package/package.json +1 -1
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +140 -134
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +6 -5
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +140 -134
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +6 -5
- package/templates/scaffold/.ksor/people.yaml +2 -2
- package/templates/scaffold/AGENTS.md +23 -8
- package/templates/scaffold/README.md +5 -2
- package/templates/scaffold/env.example +3 -0
- package/templates/scaffold/instance.md +2 -3
- package/templates/scaffold/system/site/next-no-telemetry.mjs +27 -0
- package/templates/scaffold/system/site/package.json +2 -2
- package/templates/scaffold/system/site/record/change-control.ts +319 -0
- package/templates/scaffold/system/site/record/index.ts +8 -0
- package/templates/scaffold/system/site/record/lock.ts +37 -0
- package/templates/scaffold/system/site/record/refusal.ts +1 -0
|
@@ -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-CEsb-e8z.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
|
/**
|
|
@@ -9238,6 +9238,15 @@ var OpenAiEmbeddingProvider = class {
|
|
|
9238
9238
|
* directly, so it is not ported.)
|
|
9239
9239
|
*/
|
|
9240
9240
|
/**
|
|
9241
|
+
* The stable first stderr line for a missing provider key, on BOTH planes:
|
|
9242
|
+
* `ksor serve` (through the gateway's `bootErrorLines`) and `ksor ingest` /
|
|
9243
|
+
* `ksor calibrate` (the write plane's `fail`). Exit 3 either way — the key is
|
|
9244
|
+
* the operator's environment — but an exit code is not a name, and this was
|
|
9245
|
+
* the one refusal in the first-hour path that printed its sentence with no
|
|
9246
|
+
* slug above it (found live, 2026-09-02).
|
|
9247
|
+
*/
|
|
9248
|
+
const PROVIDER_KEY_MISSING = "ksor-provider-key-missing";
|
|
9249
|
+
/**
|
|
9241
9250
|
* A key-needing provider was built without an API key. A TYPED error (mirrors
|
|
9242
9251
|
* EmbeddingSpaceMismatch) so a composition root classifies the missing-key case
|
|
9243
9252
|
* by TYPE, not by string-matching this message — the exact prose-coupling scar
|
|
@@ -9245,6 +9254,7 @@ var OpenAiEmbeddingProvider = class {
|
|
|
9245
9254
|
* exit-code mapping no longer breaks when it is reworded.
|
|
9246
9255
|
*/
|
|
9247
9256
|
var MissingProviderKeyError = class extends Error {
|
|
9257
|
+
slug = PROVIDER_KEY_MISSING;
|
|
9248
9258
|
providerName;
|
|
9249
9259
|
keyEnv;
|
|
9250
9260
|
/**
|
|
@@ -9577,6 +9587,34 @@ function keywordAbstains(topRank, config) {
|
|
|
9577
9587
|
if (config.keywordFloor === null) return topRank === null;
|
|
9578
9588
|
return topRank === null || topRank < config.keywordFloor;
|
|
9579
9589
|
}
|
|
9590
|
+
const PUBLISHED_SQL = `
|
|
9591
|
+
SELECT c.active_generation AS generation,
|
|
9592
|
+
r.source_commit,
|
|
9593
|
+
(SELECT count(*)::int FROM content_nodes n
|
|
9594
|
+
WHERE n.tenant_id = c.tenant_id AND n.generation = c.active_generation) AS nodes
|
|
9595
|
+
FROM corpora c
|
|
9596
|
+
LEFT JOIN ingestion_runs r
|
|
9597
|
+
ON r.tenant_id = c.tenant_id AND r.corpus_id = c.corpus_id
|
|
9598
|
+
AND r.generation = c.active_generation
|
|
9599
|
+
WHERE c.tenant_id = $1 AND c.corpus_id = $2`;
|
|
9600
|
+
/**
|
|
9601
|
+
* One statement on an already-scoped client, so a readiness probe can read it
|
|
9602
|
+
* in place of its own `SELECT 1` and keep the answer fresh without a second
|
|
9603
|
+
* statement per probe.
|
|
9604
|
+
*/
|
|
9605
|
+
async function readPublished(client, instance) {
|
|
9606
|
+
const row = (await client.query(PUBLISHED_SQL, [instance.tenantId, instance.corpusId])).rows[0];
|
|
9607
|
+
const generation = Number(row?.generation ?? 0);
|
|
9608
|
+
if (row === void 0 || generation === 0) return null;
|
|
9609
|
+
return {
|
|
9610
|
+
generation,
|
|
9611
|
+
nodes: Number(row.nodes ?? 0),
|
|
9612
|
+
sourceCommit: row.source_commit ?? "unspecified"
|
|
9613
|
+
};
|
|
9614
|
+
}
|
|
9615
|
+
async function publishedGeneration(pool, instance) {
|
|
9616
|
+
return runRead(pool, instance.tenantId, (client) => readPublished(client, instance));
|
|
9617
|
+
}
|
|
9580
9618
|
/** The serving subset of the schema CHECK vocabulary. */
|
|
9581
9619
|
const READ_ACTIONS = /* @__PURE__ */ new Set([
|
|
9582
9620
|
"similarity_searched",
|
|
@@ -10551,6 +10589,39 @@ async function search(ctx, query, k = 10) {
|
|
|
10551
10589
|
textSearchConfig: inst.textSearchConfig,
|
|
10552
10590
|
pinnedGeneration: null
|
|
10553
10591
|
};
|
|
10592
|
+
if (await runRead(ctx.pool, inst.tenantId, async (client) => {
|
|
10593
|
+
const r = await client.query("SELECT active_generation FROM corpora WHERE tenant_id = $1 AND corpus_id = $2", [inst.tenantId, inst.corpusId]);
|
|
10594
|
+
return Number(r.rows[0]?.active_generation ?? 0);
|
|
10595
|
+
}, servingScope(ctx)) === 0) {
|
|
10596
|
+
const audited = await logRead(ctx.pool, {
|
|
10597
|
+
tenantId: inst.tenantId,
|
|
10598
|
+
corpusId: inst.corpusId,
|
|
10599
|
+
actor,
|
|
10600
|
+
action: "search_abstained",
|
|
10601
|
+
instanceDigest: ctx.instanceDigest,
|
|
10602
|
+
detail: {
|
|
10603
|
+
...actScope(ctx),
|
|
10604
|
+
abstained: true,
|
|
10605
|
+
result_count: 0,
|
|
10606
|
+
query_chars: queryChars,
|
|
10607
|
+
k,
|
|
10608
|
+
k_effective: kb,
|
|
10609
|
+
top_cosine: null,
|
|
10610
|
+
degraded: false
|
|
10611
|
+
}
|
|
10612
|
+
});
|
|
10613
|
+
return {
|
|
10614
|
+
ok: false,
|
|
10615
|
+
abstained: false,
|
|
10616
|
+
reason: "unpublished",
|
|
10617
|
+
gate: gateState(inst),
|
|
10618
|
+
top_cosine: null,
|
|
10619
|
+
hits: [],
|
|
10620
|
+
snapshot: null,
|
|
10621
|
+
...kNote === void 0 ? {} : { k_note: kNote },
|
|
10622
|
+
...audited ? {} : { audit: "degraded" }
|
|
10623
|
+
};
|
|
10624
|
+
}
|
|
10554
10625
|
let queryVector = null;
|
|
10555
10626
|
let degradedReason;
|
|
10556
10627
|
let embedFailed = false;
|
|
@@ -10602,11 +10673,7 @@ async function search(ctx, query, k = 10) {
|
|
|
10602
10673
|
degraded: degradedReason !== void 0
|
|
10603
10674
|
}
|
|
10604
10675
|
});
|
|
10605
|
-
const
|
|
10606
|
-
const r = await client.query("SELECT active_generation FROM corpora WHERE tenant_id = $1 AND corpus_id = $2", [inst.tenantId, inst.corpusId]);
|
|
10607
|
-
return Number(r.rows[0]?.active_generation ?? 0) === 0;
|
|
10608
|
-
}, servingScope(ctx));
|
|
10609
|
-
const reason = embedFailed ? "unavailable" : unpublished ? "unpublished" : "abstained";
|
|
10676
|
+
const reason = embedFailed ? "unavailable" : "abstained";
|
|
10610
10677
|
return {
|
|
10611
10678
|
ok: false,
|
|
10612
10679
|
abstained: reason === "abstained",
|
|
@@ -11156,4 +11223,4 @@ function readHandler(ctx) {
|
|
|
11156
11223
|
};
|
|
11157
11224
|
}
|
|
11158
11225
|
//#endregion
|
|
11159
|
-
export { parseViewer as A,
|
|
11226
|
+
export { parseViewer as A, servingPolicy as B, contentPoolMin as C, outlineHandler as D, keyRingFromEnv as E, readHandler as F, withPgRetry as G, tallyHandlers as H, readPublished as I, withProbeDeadline as K, recordIsUndescribed as L, prewarmPool as M, providerKeyEnv as N, parseInstanceText as O, publishedGeneration as P, runProbe as R, contentPool as S, instancePathOf as T, tlsPosture as U, storedTextSearchConfig as V, validateViewer as W, assertGovernanceServable as _, GovernanceGateError as a, checkEmbeddingSpace as b, McpServer$1 as c, READ_ONLY as d, READ_OUTPUT as f, TextSearchConfigMismatch as g, TRUST_TIERS as h, FLOOR as i, pooledEndpointFor as j, parseTrustFloor as k, MissingProviderKeyError as l, SchemaVersionError as m, ContentStoreError as n, MAX_OUTLINE_LIMIT as o, SEARCH_OUTPUT as p, z$1 as q, EmbeddingSpaceMismatch as r, MAX_SEARCH_K as s, AudienceError as t, OUTLINE_OUTPUT as u, assertSchemaCompatible as v, embedQueryVlit as w, composeInstructions as x, buildShippedProvider as y, searchHandler as z };
|
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-Cla3mzJ0.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-gfAuwbwI.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
|
|
1
|
+
import { D as outlineHandler, F as readHandler, L as recordIsUndescribed, 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, q as z$1, s as MAX_SEARCH_K, u as OUTLINE_OUTPUT, x as composeInstructions, z as searchHandler } from "./gateway-api-CEsb-e8z-Ch6oMaq2.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/building.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Building
|
|
3
|
+
status: draft
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `ksor build`, and handing the record to something else
|
|
7
|
+
|
|
8
|
+
`ksor build` is the database-free verb that makes the SITE correct: it
|
|
9
|
+
regenerates every `index.md` in memory, runs the record checker, and on green
|
|
10
|
+
writes the indexes whose bytes changed plus `build.lock.json` — the provenance
|
|
11
|
+
every machine artefact stamps. `pnpm build` in a scaffold runs it before the
|
|
12
|
+
site build, so most owners never type it. The verb's own page is
|
|
13
|
+
`ksor build --help`; the flags that matter here:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ksor build # check, regenerate, write the lock
|
|
17
|
+
ksor build --as-of 2026-09-01T00:00:00Z # pin the instant lifecycle is judged at
|
|
18
|
+
ksor build --strict # refuse an uncommitted input (a release)
|
|
19
|
+
ksor build --bundles # also write one OKF bundle per viewer
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A refusal exits `1` with its slug on the first stderr line and writes nothing.
|
|
23
|
+
|
|
24
|
+
## `--bundles`: one OKF bundle per viewer
|
|
25
|
+
|
|
26
|
+
The record is an OKF bundle in the KSoR Profile, so `knowledge/` handed to any
|
|
27
|
+
OKF consumer already reads as a conformant bundle. What it does NOT do is
|
|
28
|
+
respect audience: the committed tree holds every document for every reader,
|
|
29
|
+
drafts and internal ones included, because anyone with the repository has the
|
|
30
|
+
files anyway. `--bundles` is the projection for the case where you hand the
|
|
31
|
+
record to someone who should see only part of it.
|
|
32
|
+
|
|
33
|
+
For `public`, and for each audience `X` registered in `.ksor/governance.yaml`,
|
|
34
|
+
it writes `.ksor/out/bundles/<viewer>/` built for the viewer list
|
|
35
|
+
`[public, X]` exactly — the same admission the site and the MCP door use, taken
|
|
36
|
+
from the lock's own `admitted` set:
|
|
37
|
+
|
|
38
|
+
- the admitted concepts only: `stable`, past `effective_from`, before
|
|
39
|
+
`stale_after`, not taken down, audience overlapping;
|
|
40
|
+
- their companions (`x.summary.md`, `x.flashcards.yaml`, …) beside them;
|
|
41
|
+
- the assets their bodies reference — an image nothing published mentions does
|
|
42
|
+
not travel;
|
|
43
|
+
- every `index.md` regenerated for that filtered tree, with `okf_version` at
|
|
44
|
+
the root, so a folder with nothing admitted has no index and no bullet in
|
|
45
|
+
its parent;
|
|
46
|
+
- frontmatter verbatim, unknown keys preserved, bytes unchanged.
|
|
47
|
+
|
|
48
|
+
No byte of a concept excluded for AUDIENCE reaches a bundle: not its title, its
|
|
49
|
+
path, its description, a companion of it, or an asset only it references. The
|
|
50
|
+
test that holds this greps the emitted tree for the excluded document rather
|
|
51
|
+
than inspecting it by name, because a bundle is the one projection that leaves
|
|
52
|
+
the building. What makes it byte-complete is the record checker, not this
|
|
53
|
+
command: a link, a supersession pointer or a companion body reaching a narrower
|
|
54
|
+
audience is refused as `ksor-link-widens` before a bundle is ever planned, so a
|
|
55
|
+
body copied verbatim cannot name what its own readers may not open.
|
|
56
|
+
|
|
57
|
+
An exclusion for a LIFECYCLE or LEDGER reason — a draft, a document not yet
|
|
58
|
+
effective, one past `stale_after`, one taken down — is narrower on purpose. The
|
|
59
|
+
body is copied verbatim, never rewritten, so if a held document links to one of
|
|
60
|
+
those, the excluded path ships inside that link. It is [reported per
|
|
61
|
+
bundle](#what-it-will-tell-you) rather than edited away: rewriting a body would
|
|
62
|
+
make the bundle a derivative instead of a copy you can hash against the record,
|
|
63
|
+
and the reader who sees the path is already entitled to that audience.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
.ksor/out/bundles/
|
|
67
|
+
├── build.lock.json # a copy — the bundles travel with the build that made them
|
|
68
|
+
├── public/
|
|
69
|
+
│ ├── index.md # okf_version: "0.2", bullets for what public may read
|
|
70
|
+
│ ├── what-is-a-ksor.md
|
|
71
|
+
│ ├── what-is-a-ksor.summary.md
|
|
72
|
+
│ └── surfaces/
|
|
73
|
+
│ ├── index.md
|
|
74
|
+
│ └── …
|
|
75
|
+
└── internal/ # the viewer [public, internal]: everything above, plus
|
|
76
|
+
├── index.md
|
|
77
|
+
├── board-pay.md
|
|
78
|
+
└── …
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The directory is REPLACED on every `--bundles` run, so a bundle for an audience
|
|
82
|
+
the policy no longer registers does not sit beside the fresh ones. It is
|
|
83
|
+
gitignored by the scaffold's `.ksor/*` rule; nothing under it is the record.
|
|
84
|
+
|
|
85
|
+
### What the lock records
|
|
86
|
+
|
|
87
|
+
`build.lock.json` carries `bundles[]` — one `{ viewer, sha256, files }` per
|
|
88
|
+
viewer — on EVERY build, whether or not `--bundles` was passed. The bundles are
|
|
89
|
+
a function of what the lock already hashes, so recording them does not depend
|
|
90
|
+
on the flag and does not move `build_id`. The digest is sha256 over the JSON of
|
|
91
|
+
the bundle's sorted `[path, sha256]` pairs, stated this plainly so a recipient
|
|
92
|
+
holding only the directory can recompute it and match it to a publication:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { createHash } from "node:crypto";
|
|
96
|
+
const sha = (b) => createHash("sha256").update(b).digest("hex");
|
|
97
|
+
const pairs = files // [[relativePath, bytes], …] — every file in the bundle
|
|
98
|
+
.map(([rel, bytes]) => [rel, sha(bytes)])
|
|
99
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
100
|
+
const digest = sha(JSON.stringify(pairs)); // equals lock.bundles[i].sha256
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### What it will tell you
|
|
104
|
+
|
|
105
|
+
A body is copied verbatim, never rewritten. If an admitted document links to a
|
|
106
|
+
concept the bundle excludes for a lifecycle or ledger reason — a draft, one not
|
|
107
|
+
yet effective, one past its review date, one taken down — the link dangles for
|
|
108
|
+
that bundle's reader, and the build says so on stdout:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
wrote .ksor/out/bundles/public/ — the OKF bundle for viewer [public], 8 file(s)
|
|
112
|
+
policies/board-pay.md links to policies/purchase-approval.md, which this bundle excludes — the link dangles for its reader
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A link that would widen AUDIENCE never gets this far: the record checker
|
|
116
|
+
refuses it (`ksor-link-widens`) before any bundle is planned, because a public
|
|
117
|
+
document naming an internal one leaks the name whether or not the target
|
|
118
|
+
travels.
|
|
119
|
+
|
|
120
|
+
Two refusals come from the bundles, and both run on EVERY build, with or
|
|
121
|
+
without the flag — the lock records `bundles[]` either way, and a digest for a
|
|
122
|
+
directory the tool refuses to write would be provenance for something that
|
|
123
|
+
cannot exist.
|
|
124
|
+
|
|
125
|
+
An audience identifier is a directory name here, so one that cannot be a path
|
|
126
|
+
segment is refused before anything is written:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
error: ksor-audience-identifier-invalid
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Name audiences in plain words — a letter or a digit first, then letters,
|
|
133
|
+
digits, `-`, `_` and `.` — in `.ksor/governance.yaml` and in every
|
|
134
|
+
`ksor.audience` list. That first-character rule is why `../escape`, `.hidden`
|
|
135
|
+
and `-x` are all refused, and `build.lock.json` is refused too: the lock copy
|
|
136
|
+
sits beside the bundle directories.
|
|
137
|
+
|
|
138
|
+
Two registered audiences that differ only in case — `internal` and `Internal` —
|
|
139
|
+
are two viewers and one directory on macOS and on Windows, whose filesystems
|
|
140
|
+
are case-insensitive by default. The second bundle written would merge into the
|
|
141
|
+
first, leaving a directory holding concepts the viewer named on it may not read
|
|
142
|
+
and a lock digest that no longer describes it, so they are refused on every
|
|
143
|
+
platform alike:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
error: ksor-audience-identifier-collides
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Give each audience a name that differs by more than case. `public` is reserved,
|
|
150
|
+
casefolded too.
|
|
151
|
+
|
|
152
|
+
### What it is not
|
|
153
|
+
|
|
154
|
+
It is not import: reading a foreign bundle into a record is not built. And it
|
|
155
|
+
is not the site or the door — a bundle carries no `llms.txt`, no search index,
|
|
156
|
+
no citations. It is the record, filtered to one viewer, in the format the record
|
|
157
|
+
is already written in.
|
package/docs/deploying.md
CHANGED
|
@@ -314,8 +314,8 @@ correct for a genuinely public record or one behind your own gateway; it is not
|
|
|
314
314
|
way to make a deploy go green.
|
|
315
315
|
|
|
316
316
|
The alternative is a real authorization server — `KSOR_SSO_URL`,
|
|
317
|
-
`KSOR_MCP_RESOURCE_URL`, `KSOR_JWT_ALLOWED_AUDIENCES`, with worked recipes for
|
|
318
|
-
of them in [authorization.md](./authorization.md).
|
|
317
|
+
`KSOR_MCP_RESOURCE_URL`, `KSOR_JWT_ALLOWED_AUDIENCES`, with worked recipes for
|
|
318
|
+
four of them in [authorization.md](./authorization.md).
|
|
319
319
|
|
|
320
320
|
### Set this on any container host
|
|
321
321
|
|
|
@@ -378,7 +378,14 @@ to anyone who types the URL, so there is nothing for rebinding to steal.
|
|
|
378
378
|
database: it regenerates every `index.md`, runs the record checker, and writes
|
|
379
379
|
`build.lock.json` — commit it — which every machine artefact stamps. A refusal
|
|
380
380
|
stops the build before a byte is written; `--strict` also refuses an
|
|
381
|
-
uncommitted input.
|
|
381
|
+
uncommitted input. One refusal reads git: a `stable` document whose body
|
|
382
|
+
changed while its `generated.at` did not ADVANCE — left alone, or moved
|
|
383
|
+
backward — is `ksor-generated-stale`, compared against every committed version
|
|
384
|
+
of the file, so when you edit a published document, move the stamp forward and
|
|
385
|
+
re-approve. A checkout without history cannot
|
|
386
|
+
read those versions; the build then prints `change-control: not checked` (or,
|
|
387
|
+
on a shallow clone, how many versions it did read) beside the `source:` line
|
|
388
|
+
instead of passing quietly. Takedowns reach the site through the committed ledger
|
|
382
389
|
(`.ksor/takedowns.yaml`), which is a file in the repository — so the site build
|
|
383
390
|
needs no `KSOR_DB_URL` at all.
|
|
384
391
|
|
|
@@ -573,7 +580,7 @@ auth-off default, and that refusal is the last real step of a deployment. Two
|
|
|
573
580
|
ways past it:
|
|
574
581
|
|
|
575
582
|
- **Configure the SSO door** — `KSOR_SSO_URL`, `KSOR_MCP_RESOURCE_URL`,
|
|
576
|
-
`KSOR_JWT_ALLOWED_AUDIENCES`. Worked recipes for
|
|
583
|
+
`KSOR_JWT_ALLOWED_AUDIENCES`. Worked recipes for four different authorization
|
|
577
584
|
servers: [authorization.md](./authorization.md).
|
|
578
585
|
- **Set `KSOR_AUTH=disabled-public`** — a deliberate decision that
|
|
579
586
|
serves your whole record to anyone who can reach the port. Correct for a
|
|
@@ -585,13 +592,18 @@ Check which one you got. `/health` says so plainly:
|
|
|
585
592
|
```json
|
|
586
593
|
{
|
|
587
594
|
"corpus_id": "book",
|
|
588
|
-
"
|
|
595
|
+
"generation": "1 · 81 nodes · source 3807493d3f2f1b4c2e6b0a9d8c7f6e5d4c3b2a10",
|
|
596
|
+
"abstain_gate": "OFF — no floor calibrated; out-of-corpus questions will be answered, not refused",
|
|
589
597
|
"embedding_space": "gemini-embedding-001/d1536 ok",
|
|
590
598
|
"auth": "disabled"
|
|
591
599
|
}
|
|
592
600
|
```
|
|
593
601
|
|
|
594
602
|
`"auth":"disabled"` on a public host means the second option is in effect.
|
|
603
|
+
`"generation"` is what the door is serving — `NONE — nothing published; run
|
|
604
|
+
pnpm refresh` on a record that was provisioned and never ingested, which is
|
|
605
|
+
where skipping the publish step leaves it — and it is re-read on every
|
|
606
|
+
readiness probe, so a `refresh` after boot shows here without a restart.
|
|
595
607
|
|
|
596
608
|
## What a cold start costs
|
|
597
609
|
|
package/docs/index.md
CHANGED
|
@@ -43,6 +43,10 @@ instead of their training memory. The corpus grows with each implemented verb.
|
|
|
43
43
|
`system/gateways/content.ts` is emitted, adopter-owned and deletable; it
|
|
44
44
|
decides tool names, what the record says it covers, and how much of the
|
|
45
45
|
caller's context an answer costs. Includes the measurements.
|
|
46
|
+
- **[building.md](./building.md)** — `ksor build`, and `--bundles`: one OKF
|
|
47
|
+
bundle per viewer under `.ksor/out/bundles/`, holding only what that
|
|
48
|
+
viewer's machine surfaces publish, for handing the record to another
|
|
49
|
+
system with no ksor in the loop. Includes how the lock records each one.
|
|
46
50
|
- **[deploying.md](./deploying.md)** — getting both surfaces onto a host. The
|
|
47
51
|
scaffold emits a `Dockerfile` that names no vendor, and a `vercel.json` that
|
|
48
52
|
points at it to put the site and the MCP door behind one domain. Includes
|
|
@@ -57,14 +61,14 @@ instead of their training memory. The corpus grows with each implemented verb.
|
|
|
57
61
|
which a dependency bump reaches a project already scaffolded. Includes the
|
|
58
62
|
list of files migrate does NOT carry, so you know what to diff by hand.
|
|
59
63
|
- **[authorization.md](./authorization.md)** — putting the record behind an
|
|
60
|
-
authorization server, with worked recipes for
|
|
61
|
-
than written. `ksor serve` refuses to boot unauthenticated on a public bind,
|
|
64
|
+
authorization server, with worked recipes for four of them — two self-hosted,
|
|
65
|
+
one hosted, one an organisation's own — executed rather than written. `ksor serve` refuses to boot unauthenticated on a public bind,
|
|
62
66
|
so this is the last step of a deployment, not an optional hardening pass.
|
|
63
67
|
- Exit codes are a contract: `1` refused (first stderr line is a stable
|
|
64
68
|
slug such as `error: bad-name`, followed by a remedy), `2` designed but
|
|
65
69
|
not implemented, `3` the environment cannot run ksor
|
|
66
70
|
(`error: unsupported-platform`, `error: broken-install`,
|
|
67
|
-
`error: environment`).
|
|
71
|
+
`error: ksor-provider-key-missing`, `error: environment`).
|
|
68
72
|
- The package root exports the CLI contract: `exitCodes`, `verbs`, and
|
|
69
73
|
`resolveCommand`.
|
|
70
74
|
|
package/docs/ingesting.md
CHANGED
|
@@ -50,7 +50,7 @@ root**, where `instance.md` and `package.json` live.
|
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
52
|
pnpm provision # ksor schema --apply, then ksor grant
|
|
53
|
-
pnpm refresh # ksor ingest --flip, then ksor gc
|
|
53
|
+
pnpm refresh # ksor build, then ksor ingest --flip, then ksor gc
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
`provision` is separate because applying DDL and granting ingest are acts an
|
|
@@ -88,6 +88,26 @@ website's lock was fixed for exactly this once already, after deleting a
|
|
|
88
88
|
denial's four lines republished the document with the committed lock still
|
|
89
89
|
validating; ingest had the same hole, found by review before anyone hit it.
|
|
90
90
|
|
|
91
|
+
## When ingest says generated.at is stale
|
|
92
|
+
|
|
93
|
+
`ksor ingest` runs the same change-control check `ksor build` runs (KSP R23;
|
|
94
|
+
the scaffold's `pnpm check` does not — it is the format gate, and reads no
|
|
95
|
+
document history): a `stable` document whose body differs from a committed version
|
|
96
|
+
that was `stable`, without `generated.at` having advanced past that version's,
|
|
97
|
+
is refused `ksor-generated-stale`, before anything is written. The stamp dates
|
|
98
|
+
the text, so leaving it alone and moving it backward are refused alike.
|
|
99
|
+
The fix is the one it prints — set `generated.at` to an instant after the edit,
|
|
100
|
+
then re-approve, because `ksor.approval.at` may not precede it — and the check
|
|
101
|
+
reads every committed version of the file, so an edit committed without a bump
|
|
102
|
+
is refused on a clean tree too.
|
|
103
|
+
|
|
104
|
+
Ingest reads git for this, and a container that received the record without
|
|
105
|
+
its `.git` cannot. It then prints `change-control: not checked` on stderr and
|
|
106
|
+
continues: a check that could not run is reported, never counted as passed.
|
|
107
|
+
Who approved is still checked against the policy alone — every envelope says
|
|
108
|
+
`approval.checked: "policy"` — because R22 and R25 wait on an identity the
|
|
109
|
+
platform can vouch for.
|
|
110
|
+
|
|
91
111
|
## What a generation is
|
|
92
112
|
|
|
93
113
|
Each ingest builds a **fresh generation** — invisible until activated — and
|
|
@@ -204,7 +224,10 @@ LLM, and question synthesis is Gemini-only today — so a record on
|
|
|
204
224
|
refused here for a Google key. That is a real gap, stated rather than papered
|
|
205
225
|
over; the zero-LLM door below avoids it entirely and is the better choice on a
|
|
206
226
|
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.
|
|
227
|
+
and a bigger corpus makes that worse, not better. Zero-LLM is not zero-key: the
|
|
228
|
+
questions are still embedded, so the embedding provider's own key
|
|
229
|
+
(`GEMINI_API_KEY` or `OPENAI_API_KEY`, whichever `embedding.provider` names) is
|
|
230
|
+
still required — only the question synthesis is skipped.
|
|
208
231
|
|
|
209
232
|
Write your own in-corpus questions, one per line, and pass them:
|
|
210
233
|
|
|
@@ -289,9 +312,12 @@ decision in this project's own gold rather than a threshold somebody picked.
|
|
|
289
312
|
Run it on a schedule, or in your own CI beside `ksor build`. Three things to
|
|
290
313
|
know about what it is:
|
|
291
314
|
|
|
292
|
-
- **
|
|
293
|
-
re-measuring, and failing a build for one would make the
|
|
294
|
-
deleting `vector_floor` — turning the gate off entirely to
|
|
315
|
+
- **A verdict never fails a run.** STEADY, WATCH and no-data all exit 0: a
|
|
316
|
+
stale floor wants re-measuring, and failing a build for one would make the
|
|
317
|
+
shortest way out deleting `vector_floor` — turning the gate off entirely to
|
|
318
|
+
clear the error. What does exit non-zero is the environment, the same way it
|
|
319
|
+
does for every verb: 3 when `KSOR_DB_URL` is unset or the database is
|
|
320
|
+
unreachable, 1 on a bad flag. Put it in CI knowing that.
|
|
295
321
|
- **It reads traffic, so it needs traffic**, and it says so rather than
|
|
296
322
|
reporting a healthy-looking nothing. It also cannot see questions nobody
|
|
297
323
|
asked: it can tell you the floor has gone permissive, never that it is too
|
|
@@ -326,7 +352,11 @@ pull request is refused exactly as the verb would refuse it.
|
|
|
326
352
|
Lifting a takedown is `--revoke <entry-id>` — the id of the LEDGER ENTRY, not
|
|
327
353
|
the stable id. The denial that created it prints the id, `ksor takedown
|
|
328
354
|
--ledger` lists it, and it is written in `.ksor/takedowns.yaml`; none of the
|
|
329
|
-
three needs a database, because the ledger is a file in the repository
|
|
355
|
+
three needs a database, because the ledger is a file in the repository —
|
|
356
|
+
`--ledger` reads that file and never asks for a DSN, whatever `instance.md`
|
|
357
|
+
declares. (`--list` is a question about the door's rows, so it reads them when
|
|
358
|
+
the DSN is set; with none it lists the ledger's denials, each labelled
|
|
359
|
+
`not applied (no database)`.) The
|
|
330
360
|
ledger is append-only: a revocation is a new entry, never a deleted line, and a
|
|
331
361
|
build whose ledger shrank against its own git history is refused.
|
|
332
362
|
|
package/docs/tool-surface.md
CHANGED
|
@@ -144,9 +144,6 @@ made it answer from.
|
|
|
144
144
|
only things that can prove a passage came from the governed record. A
|
|
145
145
|
hand-written one returning fabricated hits with plausible `stable_id`s would
|
|
146
146
|
pass every shape check there is.
|
|
147
|
-
- **The output schemas.** `SEARCH_OUTPUT`, `OUTLINE_OUTPUT`, `READ_OUTPUT` carry
|
|
148
|
-
`provenance`, each hit's `governance`, the `snapshot` token and `gate`. A
|
|
149
|
-
record that reshaped them would still look like a KSoR and no longer be one.
|
|
150
147
|
- **The output schemas.** `SEARCH_OUTPUT`, `OUTLINE_OUTPUT`, `READ_OUTPUT` carry
|
|
151
148
|
`provenance`, each hit's `governance`, the `snapshot` token, `gate`, and
|
|
152
149
|
`audit`. A record that reshaped them would still look like a KSoR and no
|
package/docs/upgrading.md
CHANGED
|
@@ -45,6 +45,7 @@ easier to read when nothing else is uncommitted.
|
|
|
45
45
|
| `.gitignore` | the entries a new release needs negated |
|
|
46
46
|
| `.agents/` and `.claude/` format-checker | the emitted checker, so your own `pnpm check` and your CI agree with the tool |
|
|
47
47
|
| root `package.json` **scripts** | scripts a release broke — a removed flag, a step that now needs `ksor build` in front of it |
|
|
48
|
+
| `build.lock.json` | DELETED when this ksor cannot read it — see below |
|
|
48
49
|
| `system/site/**` | **only with `--write-site`** — every file of the site this release emits |
|
|
49
50
|
|
|
50
51
|
`--write-site` is the one to remember, because it is the only path by which a
|
|
@@ -54,6 +55,19 @@ security bump reaches an existing project: the site's `package.json` is where
|
|
|
54
55
|
It is an **update, never a creation**. A record with no `system/site` of its own
|
|
55
56
|
is not given one.
|
|
56
57
|
|
|
58
|
+
### A lock a newer ksor cannot read is deleted, never rewritten
|
|
59
|
+
|
|
60
|
+
`build.lock.json` gains fields as the record's surfaces grow, and `ksor build`
|
|
61
|
+
REFUSES a lock missing one (`ksor-lock-invalid`) instead of regenerating it: the
|
|
62
|
+
lock is also a takedown baseline, and a baseline nothing can parse is one that
|
|
63
|
+
quietly holds nothing. So the file has to GO before the first build on a new
|
|
64
|
+
ksor, and step 3 is what removes it — the diff shows the deletion like any
|
|
65
|
+
other. Step 4 then writes the current one.
|
|
66
|
+
|
|
67
|
+
Skip the migration and step 4 tells you the same thing: `ksor build` exits 1
|
|
68
|
+
naming the file, and so does `pnpm build`, which runs it first. Nothing is lost
|
|
69
|
+
— a lock is computed from the tree, never edited.
|
|
70
|
+
|
|
57
71
|
### The site manifest is merged, not replaced
|
|
58
72
|
|
|
59
73
|
Every other file under `system/site` is reissued whole. `system/site/package.json`
|
|
@@ -76,7 +90,7 @@ These are yours, and no release touches them. Diff them against a fresh
|
|
|
76
90
|
- `pnpm-workspace.yaml` / `.npmrc` and any lockfile
|
|
77
91
|
|
|
78
92
|
```sh
|
|
79
|
-
npx @panaversity/ksor@latest init
|
|
93
|
+
(cd /tmp && npx @panaversity/ksor@latest init fresh) # init takes a NAME, not a path
|
|
80
94
|
diff -ru /tmp/fresh/vercel.json ./vercel.json
|
|
81
95
|
```
|
|
82
96
|
|
|
@@ -97,7 +111,7 @@ that supplies it. Two you will meet often:
|
|
|
97
111
|
|
|
98
112
|
## After it applies
|
|
99
113
|
|
|
100
|
-
`pnpm build` regenerates every index and
|
|
114
|
+
`pnpm build` regenerates every index and writes a fresh `build.lock.json`; `pnpm check`
|
|
101
115
|
runs the record checker that shipped with the new tool. If you serve the record,
|
|
102
116
|
`ksor schema --apply` and then a full `pnpm refresh` publish a generation the new
|
|
103
117
|
door can read — and a calibrated `vector_floor` measured under an older serving
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panaversity/ksor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.59",
|
|
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",
|