@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
|
@@ -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
|
/**
|
|
@@ -8899,7 +8899,7 @@ var FakeEmbeddingProvider = class {
|
|
|
8899
8899
|
}
|
|
8900
8900
|
reset() {}
|
|
8901
8901
|
};
|
|
8902
|
-
const DEFAULT_BASE = "https://generativelanguage.googleapis.com/v1beta";
|
|
8902
|
+
const DEFAULT_BASE$1 = "https://generativelanguage.googleapis.com/v1beta";
|
|
8903
8903
|
/**
|
|
8904
8904
|
* An HTTP-shaped failure carrying the status the retry classifier reads.
|
|
8905
8905
|
*
|
|
@@ -8917,7 +8917,7 @@ var GeminiHttpError = class extends Error {
|
|
|
8917
8917
|
};
|
|
8918
8918
|
/** One POST, with the key in a HEADER — never the query string, which is logged. */
|
|
8919
8919
|
async function post(opts, apiKey, path, body, timeoutMs) {
|
|
8920
|
-
const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl ?? DEFAULT_BASE}${path}`, {
|
|
8920
|
+
const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl ?? DEFAULT_BASE$1}${path}`, {
|
|
8921
8921
|
method: "POST",
|
|
8922
8922
|
headers: {
|
|
8923
8923
|
"x-goog-api-key": apiKey,
|
|
@@ -8973,33 +8973,33 @@ function geminiRestEmbedClient(apiKey, opts = {}) {
|
|
|
8973
8973
|
* Python-SDK failure mode with no JS equivalent; `reset()`
|
|
8974
8974
|
* keeps its drop-never-close contract regardless.
|
|
8975
8975
|
*/
|
|
8976
|
-
function httpStatusOf(exc) {
|
|
8976
|
+
function httpStatusOf$1(exc) {
|
|
8977
8977
|
if (!(exc instanceof Error)) return void 0;
|
|
8978
8978
|
const status = exc.status;
|
|
8979
8979
|
return typeof status === "number" ? status : void 0;
|
|
8980
8980
|
}
|
|
8981
|
-
function isTransportBlip(exc, depth = 0) {
|
|
8981
|
+
function isTransportBlip$1(exc, depth = 0) {
|
|
8982
8982
|
if (depth > 5 || !(exc instanceof Error)) return false;
|
|
8983
8983
|
if (exc.name === "AbortError" || exc.name === "TimeoutError") return true;
|
|
8984
8984
|
const code = exc.code;
|
|
8985
8985
|
if (typeof code === "string" && /^E[A-Z0-9_]+$/.test(code)) return true;
|
|
8986
8986
|
if (exc instanceof TypeError && exc.message.includes("fetch failed")) return true;
|
|
8987
|
-
return isTransportBlip(exc.cause, depth + 1);
|
|
8987
|
+
return isTransportBlip$1(exc.cause, depth + 1);
|
|
8988
8988
|
}
|
|
8989
8989
|
/** The Gemini taxonomy for the INGEST plane: transport blips, 5xx, AND 429 —
|
|
8990
8990
|
* batch work is resumable and has no user waiting. */
|
|
8991
|
-
function isRetryable(exc) {
|
|
8992
|
-
if (isTransportBlip(exc)) return true;
|
|
8993
|
-
const status = httpStatusOf(exc);
|
|
8991
|
+
function isRetryable$1(exc) {
|
|
8992
|
+
if (isTransportBlip$1(exc)) return true;
|
|
8993
|
+
const status = httpStatusOf$1(exc);
|
|
8994
8994
|
if (status === void 0) return false;
|
|
8995
8995
|
return status >= 500 && status <= 599 || status === 429;
|
|
8996
8996
|
}
|
|
8997
8997
|
/** The READ path's predicate: transport blips + 5xx only — NEVER 429. A
|
|
8998
8998
|
* rate-limited project stays rate-limited on the next second; the correct
|
|
8999
8999
|
* move is to degrade to keyword-only NOW, not stall the read behind backoff. */
|
|
9000
|
-
function isRetryableQuery(exc) {
|
|
9001
|
-
if (isTransportBlip(exc)) return true;
|
|
9002
|
-
const status = httpStatusOf(exc);
|
|
9000
|
+
function isRetryableQuery$1(exc) {
|
|
9001
|
+
if (isTransportBlip$1(exc)) return true;
|
|
9002
|
+
const status = httpStatusOf$1(exc);
|
|
9003
9003
|
return status !== void 0 && status >= 500 && status <= 599;
|
|
9004
9004
|
}
|
|
9005
9005
|
/**
|
|
@@ -9049,12 +9049,177 @@ var GeminiEmbeddingProvider = class {
|
|
|
9049
9049
|
}
|
|
9050
9050
|
})).embeddings ?? []).map((e) => [...e.values ?? []]);
|
|
9051
9051
|
}
|
|
9052
|
+
isRetryable(exc) {
|
|
9053
|
+
return isRetryable$1(exc);
|
|
9054
|
+
}
|
|
9055
|
+
isRetryableQuery(exc) {
|
|
9056
|
+
return isRetryableQuery$1(exc);
|
|
9057
|
+
}
|
|
9058
|
+
};
|
|
9059
|
+
/**
|
|
9060
|
+
* OpenAI's embedding endpoint, over `fetch` — no SDK.
|
|
9061
|
+
*
|
|
9062
|
+
* The same call decision 12's 2026-08-22 revision made for Gemini, for the same
|
|
9063
|
+
* reason: this is ONE HTTP call behind a structurally-typed slice, and a vendor
|
|
9064
|
+
* SDK would put megabytes and a transitive dependency tree into every
|
|
9065
|
+
* `ksor init` that never embeds anything. If a provider ever needs an SDK, the
|
|
9066
|
+
* seam takes one through `clientFactory`; nothing here forecloses that.
|
|
9067
|
+
*
|
|
9068
|
+
* WHAT DIFFERS FROM GEMINI, and it is worth knowing before choosing:
|
|
9069
|
+
*
|
|
9070
|
+
* Gemini embeds ASYMMETRICALLY — `RETRIEVAL_DOCUMENT` and `RETRIEVAL_QUERY`
|
|
9071
|
+
* produce different vectors for the same text, and the plane's intent picks
|
|
9072
|
+
* which. OpenAI has no task type: a query and a document are embedded
|
|
9073
|
+
* identically. The seam anticipates exactly this — "a provider whose two
|
|
9074
|
+
* vendor labels are equal can never mis-route a plane"
|
|
9075
|
+
* (`lib/embedding.ts`) — so both labels are the empty string and the intent
|
|
9076
|
+
* reaches this transport and is deliberately ignored.
|
|
9077
|
+
*
|
|
9078
|
+
* `dimensions` is supported on `text-embedding-3-*` only. Asking an older
|
|
9079
|
+
* model for a dimension is an error there, not a silent full-width vector,
|
|
9080
|
+
* which is the failure the framework's own width check would catch anyway.
|
|
9081
|
+
*/
|
|
9082
|
+
/**
|
|
9083
|
+
* An HTTP-shaped failure carrying the status the retry classifier reads — and
|
|
9084
|
+
* the vendor's own error `type`, which the status alone does not distinguish.
|
|
9085
|
+
*
|
|
9086
|
+
* OpenAI answers a spent balance with **429**, the same status as a rate limit:
|
|
9087
|
+
* `{"error":{"type":"insufficient_quota","code":"credit_balance_exhausted"}}`
|
|
9088
|
+
* (observed live, 2026-09-01). One clears by waiting and one never will, so a
|
|
9089
|
+
* classifier reading only the status retries a billing problem five times with
|
|
9090
|
+
* exponential backoff and then reports it — slowly, and as if it had been
|
|
9091
|
+
* transient.
|
|
9092
|
+
*/
|
|
9093
|
+
var OpenAiHttpError = class extends Error {
|
|
9094
|
+
status;
|
|
9095
|
+
/** The vendor's `error.type`, when it sends one. */
|
|
9096
|
+
kind;
|
|
9097
|
+
constructor(status, detail, kind = null) {
|
|
9098
|
+
super(`OpenAI API error ${status}: ${detail}`);
|
|
9099
|
+
this.name = "OpenAiHttpError";
|
|
9100
|
+
this.status = status;
|
|
9101
|
+
this.kind = kind;
|
|
9102
|
+
}
|
|
9103
|
+
};
|
|
9104
|
+
const DEFAULT_BASE = "https://api.openai.com/v1";
|
|
9105
|
+
function openAiRestEmbedClient(apiKey, opts = {}) {
|
|
9106
|
+
return { async embed(params) {
|
|
9107
|
+
const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl ?? DEFAULT_BASE}/embeddings`, {
|
|
9108
|
+
method: "POST",
|
|
9109
|
+
headers: {
|
|
9110
|
+
authorization: `Bearer ${apiKey}`,
|
|
9111
|
+
"content-type": "application/json"
|
|
9112
|
+
},
|
|
9113
|
+
body: JSON.stringify({
|
|
9114
|
+
model: params.model,
|
|
9115
|
+
input: [...params.input],
|
|
9116
|
+
dimensions: params.dimensions
|
|
9117
|
+
}),
|
|
9118
|
+
signal: AbortSignal.timeout(params.timeoutMs)
|
|
9119
|
+
});
|
|
9120
|
+
const text = await res.text();
|
|
9121
|
+
if (!res.ok) {
|
|
9122
|
+
let detail = text.slice(0, 300);
|
|
9123
|
+
let kind = null;
|
|
9124
|
+
try {
|
|
9125
|
+
const err = JSON.parse(text).error;
|
|
9126
|
+
if (typeof err?.message === "string") detail = err.message;
|
|
9127
|
+
if (typeof err?.type === "string") kind = err.type;
|
|
9128
|
+
} catch {}
|
|
9129
|
+
throw new OpenAiHttpError(res.status, detail, kind);
|
|
9130
|
+
}
|
|
9131
|
+
const data = [...JSON.parse(text).data ?? []];
|
|
9132
|
+
data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
9133
|
+
return { embeddings: data.map((d) => ({ values: d.embedding })) };
|
|
9134
|
+
} };
|
|
9135
|
+
}
|
|
9136
|
+
/** True for a transport blip with no HTTP status of its own. */
|
|
9137
|
+
function isTransportBlip(exc) {
|
|
9138
|
+
if (exc instanceof OpenAiHttpError) return false;
|
|
9139
|
+
const name = exc?.name;
|
|
9140
|
+
return name === "AbortError" || name === "TimeoutError" || name === "TypeError";
|
|
9141
|
+
}
|
|
9142
|
+
function httpStatusOf(exc) {
|
|
9143
|
+
return exc instanceof OpenAiHttpError ? exc.status : void 0;
|
|
9144
|
+
}
|
|
9145
|
+
/**
|
|
9146
|
+
* The INGEST plane's taxonomy: transport blips, 5xx, AND 429 — batch work is
|
|
9147
|
+
* resumable and has nobody waiting. Deliberately the same shape as the Gemini
|
|
9148
|
+
* adapter's, because the two planes are a property of ksor, not of a vendor.
|
|
9149
|
+
*/
|
|
9150
|
+
function isRetryable(exc) {
|
|
9151
|
+
if (isTransportBlip(exc)) return true;
|
|
9152
|
+
if (exc instanceof OpenAiHttpError && exc.kind === "insufficient_quota") return false;
|
|
9153
|
+
const status = httpStatusOf(exc);
|
|
9154
|
+
if (status === void 0) return false;
|
|
9155
|
+
return status >= 500 && status <= 599 || status === 429;
|
|
9156
|
+
}
|
|
9157
|
+
/**
|
|
9158
|
+
* The READ plane's: transport blips + 5xx only, NEVER 429. A rate-limited
|
|
9159
|
+
* project stays rate-limited on the next second, so a search degrades to
|
|
9160
|
+
* keyword-only now rather than stalling a reader behind backoff.
|
|
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
|
+
}
|
|
9170
|
+
function isRetryableQuery(exc) {
|
|
9171
|
+
if (isTransportBlip(exc)) return true;
|
|
9172
|
+
const status = httpStatusOf(exc);
|
|
9173
|
+
return status !== void 0 && status >= 500 && status <= 599;
|
|
9174
|
+
}
|
|
9175
|
+
var OpenAiEmbeddingProvider = class {
|
|
9176
|
+
providerId = "openai";
|
|
9177
|
+
modelId;
|
|
9178
|
+
dim;
|
|
9179
|
+
documentTaskLabel;
|
|
9180
|
+
queryTaskLabel;
|
|
9181
|
+
documentTimeoutMs;
|
|
9182
|
+
queryTimeoutMs;
|
|
9183
|
+
clientFactory;
|
|
9184
|
+
client = null;
|
|
9185
|
+
constructor(opts) {
|
|
9186
|
+
this.modelId = opts.modelId;
|
|
9187
|
+
this.dim = opts.dim;
|
|
9188
|
+
this.documentTaskLabel = opts.documentTaskLabel;
|
|
9189
|
+
this.queryTaskLabel = opts.queryTaskLabel;
|
|
9190
|
+
this.documentTimeoutMs = Math.trunc(opts.documentTimeoutS * 1e3);
|
|
9191
|
+
this.queryTimeoutMs = Math.trunc(opts.queryTimeoutS * 1e3);
|
|
9192
|
+
this.clientFactory = opts.clientFactory ?? (() => openAiRestEmbedClient(opts.apiKey));
|
|
9193
|
+
}
|
|
9194
|
+
get recipe() {
|
|
9195
|
+
return `${this.modelId}/d${this.dim}/${this.documentTaskLabel}`;
|
|
9196
|
+
}
|
|
9197
|
+
getClient() {
|
|
9198
|
+
this.client ??= this.clientFactory();
|
|
9199
|
+
return this.client;
|
|
9200
|
+
}
|
|
9201
|
+
/** DROP the client reference, never close it: in-flight calls finish on their
|
|
9202
|
+
* own reference and the next call rebuilds lazily. Idempotent. */
|
|
9203
|
+
reset() {
|
|
9204
|
+
this.client = null;
|
|
9205
|
+
}
|
|
9206
|
+
async embed(texts, opts) {
|
|
9207
|
+
return (await this.getClient().embed({
|
|
9208
|
+
model: this.modelId,
|
|
9209
|
+
input: texts,
|
|
9210
|
+
dimensions: this.dim,
|
|
9211
|
+
timeoutMs: opts.intent === "document" ? this.documentTimeoutMs : this.queryTimeoutMs
|
|
9212
|
+
})).embeddings.map((e) => [...e.values ?? []]);
|
|
9213
|
+
}
|
|
9052
9214
|
isRetryable(exc) {
|
|
9053
9215
|
return isRetryable(exc);
|
|
9054
9216
|
}
|
|
9055
9217
|
isRetryableQuery(exc) {
|
|
9056
9218
|
return isRetryableQuery(exc);
|
|
9057
9219
|
}
|
|
9220
|
+
isFatal(exc) {
|
|
9221
|
+
return isFatal(exc);
|
|
9222
|
+
}
|
|
9058
9223
|
};
|
|
9059
9224
|
/**
|
|
9060
9225
|
* The embedding-provider registry — a plain object, NOT any discovery
|
|
@@ -9081,20 +9246,49 @@ var GeminiEmbeddingProvider = class {
|
|
|
9081
9246
|
*/
|
|
9082
9247
|
var MissingProviderKeyError = class extends Error {
|
|
9083
9248
|
providerName;
|
|
9084
|
-
|
|
9085
|
-
|
|
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}`));
|
|
9086
9260
|
this.name = "MissingProviderKeyError";
|
|
9087
9261
|
this.providerName = providerName;
|
|
9262
|
+
this.keyEnv = keyEnv;
|
|
9088
9263
|
}
|
|
9089
9264
|
};
|
|
9090
9265
|
const PROVIDERS = {
|
|
9091
9266
|
gemini: {
|
|
9092
9267
|
build: (opts) => new GeminiEmbeddingProvider(opts),
|
|
9093
|
-
needsApiKey: true
|
|
9268
|
+
needsApiKey: true,
|
|
9269
|
+
keyEnv: "GEMINI_API_KEY",
|
|
9270
|
+
taskLabels: {
|
|
9271
|
+
document: EMBED_TASK_DOCUMENT,
|
|
9272
|
+
query: EMBED_TASK_QUERY
|
|
9273
|
+
}
|
|
9274
|
+
},
|
|
9275
|
+
openai: {
|
|
9276
|
+
build: (opts) => new OpenAiEmbeddingProvider(opts),
|
|
9277
|
+
needsApiKey: true,
|
|
9278
|
+
keyEnv: "OPENAI_API_KEY",
|
|
9279
|
+
taskLabels: {
|
|
9280
|
+
document: "",
|
|
9281
|
+
query: ""
|
|
9282
|
+
}
|
|
9094
9283
|
},
|
|
9095
9284
|
fake: {
|
|
9096
9285
|
build: (opts) => new FakeEmbeddingProvider(opts),
|
|
9097
|
-
needsApiKey: false
|
|
9286
|
+
needsApiKey: false,
|
|
9287
|
+
keyEnv: null,
|
|
9288
|
+
taskLabels: {
|
|
9289
|
+
document: EMBED_TASK_DOCUMENT,
|
|
9290
|
+
query: EMBED_TASK_QUERY
|
|
9291
|
+
}
|
|
9098
9292
|
}
|
|
9099
9293
|
};
|
|
9100
9294
|
function entryFor(name) {
|
|
@@ -9103,6 +9297,13 @@ function entryFor(name) {
|
|
|
9103
9297
|
return entry;
|
|
9104
9298
|
}
|
|
9105
9299
|
/**
|
|
9300
|
+
* The environment variable this provider's key comes from, or null when it
|
|
9301
|
+
* needs none. Unknown name → the same loud error as building it.
|
|
9302
|
+
*/
|
|
9303
|
+
function providerKeyEnv(name) {
|
|
9304
|
+
return entryFor(name).keyEnv;
|
|
9305
|
+
}
|
|
9306
|
+
/**
|
|
9106
9307
|
* The port door: the named provider bound to the DECLARED embedding space and
|
|
9107
9308
|
* the framework's timeout knobs. `modelId`/`dim` omitted = the shipped
|
|
9108
9309
|
* config space (EMBED_MODEL / EMBED_DIM, eval-locked); an instance may
|
|
@@ -9114,13 +9315,13 @@ function entryFor(name) {
|
|
|
9114
9315
|
*/
|
|
9115
9316
|
function buildShippedProvider(name, opts) {
|
|
9116
9317
|
const entry = entryFor(name);
|
|
9117
|
-
if (entry.needsApiKey && !opts.apiKey) throw new MissingProviderKeyError(name);
|
|
9318
|
+
if (entry.needsApiKey && !opts.apiKey) throw new MissingProviderKeyError(name, entry.keyEnv);
|
|
9118
9319
|
return entry.build({
|
|
9119
9320
|
apiKey: opts.apiKey ?? "",
|
|
9120
9321
|
modelId: opts.modelId ?? "gemini-embedding-001",
|
|
9121
9322
|
dim: opts.dim ?? 1536,
|
|
9122
|
-
documentTaskLabel:
|
|
9123
|
-
queryTaskLabel:
|
|
9323
|
+
documentTaskLabel: entry.taskLabels.document,
|
|
9324
|
+
queryTaskLabel: entry.taskLabels.query,
|
|
9124
9325
|
documentTimeoutS: EMBED_TIMEOUT_S(),
|
|
9125
9326
|
queryTimeoutS: QUERY_EMBED_TIMEOUT_S()
|
|
9126
9327
|
});
|
|
@@ -10955,4 +11156,4 @@ function readHandler(ctx) {
|
|
|
10955
11156
|
};
|
|
10956
11157
|
}
|
|
10957
11158
|
//#endregion
|
|
10958
|
-
export { parseViewer as A,
|
|
11159
|
+
export { parseViewer as A, tallyHandlers as B, contentPoolMin as C, outlineHandler as D, keyRingFromEnv as E, recordIsUndescribed as F, z$1 as G, validateViewer as H, runProbe as I, searchHandler as L, prewarmPool as M, providerKeyEnv as N, parseInstanceText as O, readHandler as P, servingPolicy as R, contentPool as S, instancePathOf as T, withPgRetry as U, tlsPosture as V, withProbeDeadline 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, 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, storedTextSearchConfig 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-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,
|
|
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
|
@@ -141,14 +141,38 @@ What the image deliberately does NOT contain (see `.dockerignore`):
|
|
|
141
141
|
> which is the door answering "Method Not Allowed" to a GET rather than a static
|
|
142
142
|
> 404, and is how you tell the door is routed at all.
|
|
143
143
|
>
|
|
144
|
-
> **
|
|
145
|
-
>
|
|
144
|
+
> **Set the Framework Preset to `Services` on the import screen.** Vercel's own
|
|
145
|
+
> guide states it as one of two necessary conditions:
|
|
146
|
+
>
|
|
147
|
+
> > "A project builds as services only when two conditions are both true: the
|
|
148
|
+
> > project's framework is set to Services, and `vercel.json` contains a
|
|
149
|
+
> > `services` key. If either is missing, Vercel falls back to its default
|
|
150
|
+
> > framework detection and ignores your services configuration."
|
|
151
|
+
> > — [vercel.com/kb/guide/vercel-services](https://vercel.com/kb/guide/vercel-services)
|
|
152
|
+
>
|
|
153
|
+
> That is the silent-404 shape exactly: preset `Other` → fallback detection →
|
|
154
|
+
> nothing detected → an empty output that deploys, reports Ready and takes the
|
|
155
|
+
> alias. And no file in your repository can set it for you: `framework` is not
|
|
156
|
+
> a valid top-level key while `services` is present, so this is a project
|
|
157
|
+
> setting or nothing.
|
|
158
|
+
>
|
|
159
|
+
> **One measurement here disagrees with that guide, and is recorded rather than
|
|
160
|
+
> reconciled.** On 2026-08-27 two projects were read back from the Vercel API,
|
|
161
|
+
> one reading preset `Services` and one reading `Other`, and BOTH built the
|
|
162
|
+
> `services` block and served:
|
|
146
163
|
>
|
|
147
164
|
> | project's preset | `services` block built | serves |
|
|
148
165
|
> | ---------------- | ---------------------- | -------------------------------------- |
|
|
149
166
|
> | `Services` | `site` + `door` | `/` 200 · `/llms.txt` 200 · `/mcp` 405 |
|
|
150
167
|
> | `Other` | `site` + `door` | `/` 200 · `/llms.txt` 200 · `/mcp` 405 |
|
|
151
168
|
>
|
|
169
|
+
> Both facts are real and they cannot both be the whole rule. Possible readings
|
|
170
|
+
> — the API's `framework` field being derived rather than the project setting,
|
|
171
|
+
> or Beta behaviour changing between the guide's 2026-08-12 revision and that
|
|
172
|
+
> measurement — are unverified, and guessing between them is what produced the
|
|
173
|
+
> earlier version of this page, which told you the preset was RULED OUT and so
|
|
174
|
+
> steered you away from the one step the vendor calls required. Set the preset.
|
|
175
|
+
>
|
|
152
176
|
> **One failure has been seen that none of this explains.** On a 205-document
|
|
153
177
|
> record (2026-08-26, issue #197) the install ran, `ksor build` ran, every route
|
|
154
178
|
> prerendered — and Vercel collected nothing. The deployment reported **Ready**,
|
|
@@ -262,9 +286,15 @@ are doing it wrong" — and only the first tier is true.
|
|
|
262
286
|
| variable | why |
|
|
263
287
|
| ----------------------------------------- | ---------------------------------------------------- |
|
|
264
288
|
| `KSOR_DB_URL` | the record's Postgres store |
|
|
265
|
-
|
|
|
289
|
+
| the provider key | embeds the incoming query, so retrieval works at all |
|
|
266
290
|
| `KSOR_AUTH`, **or** a configured SSO door | see below |
|
|
267
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
|
+
|
|
268
298
|
`KSOR_AUTH` takes one of two values, and the value IS the decision:
|
|
269
299
|
|
|
270
300
|
```sh
|
|
@@ -427,12 +457,21 @@ Plain `pnpm build` is `[public]`, so the safe thing is the default.
|
|
|
427
457
|
|
|
428
458
|
Two supported answers, and a third that is yours.
|
|
429
459
|
|
|
430
|
-
**
|
|
431
|
-
|
|
432
|
-
`
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
460
|
+
**Run a door per audience.** The door does NOT decide per request: it serves
|
|
461
|
+
ONE viewer list, read from `KSOR_AUDIENCE` once at boot
|
|
462
|
+
(`content-gateway/src/compose.ts`) and validated against the policy before it
|
|
463
|
+
widens past `public`. Every caller holding a valid token for that door gets the
|
|
464
|
+
same view of the record — `docs/authorization.md` says so plainly, and
|
|
465
|
+
per-request visibility filtering is named out of scope in
|
|
466
|
+
`specs/ksor/serve/spec.md`. So the answer is one process per viewer list, each
|
|
467
|
+
with its own `KSOR_AUDIENCE`, behind whatever routing already decides who
|
|
468
|
+
reaches which URL.
|
|
469
|
+
|
|
470
|
+
What the door gives that a static site cannot is the **audit**, not the
|
|
471
|
+
authorization: every read writes a `retrieval_log` row naming the verified
|
|
472
|
+
caller, so "who read what" is answerable afterwards. That is worth having, and
|
|
473
|
+
it is a different thing from "were they allowed to", which is still decided by
|
|
474
|
+
which door they can reach.
|
|
436
475
|
|
|
437
476
|
**Or split the record.** Content needing per-person confidentiality inside one
|
|
438
477
|
tier is usually content that belongs in its own record, with its own gate. That
|
package/docs/ingesting.md
CHANGED
|
@@ -35,12 +35,12 @@ Ingest reads your markdown, sends each new chunk to an embedding provider, and
|
|
|
35
35
|
writes the result to Postgres. So four things must be true, and none of them is
|
|
36
36
|
created for you.
|
|
37
37
|
|
|
38
|
-
| | what
|
|
39
|
-
| -------------------- |
|
|
40
|
-
| **The corpus** | `knowledge/` at your repo root — CommonMark `.md`, one document per file, in the KSoR Profile of OKF: `type`, `title`, `description`, `status` and `ksor.audience` in frontmatter
|
|
41
|
-
| **The database** | Postgres with **pgvector** — `CREATE EXTENSION vector;`
|
|
42
|
-
| **The provider key** | `GEMINI_API_KEY` — the default embedding provider is `gemini-embedding-001`
|
|
43
|
-
| **The DSN** | `KSOR_DB_URL`, named by `instance.md`'s `database.dsn_env`
|
|
38
|
+
| | what | how |
|
|
39
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| **The corpus** | `knowledge/` at your repo root — CommonMark `.md`, one document per file, in the KSoR Profile of OKF: `type`, `title`, `description`, `status` and `ksor.audience` in frontmatter | `pnpm check` validates it and explains any violation; `ksor build` must have written a current `build.lock.json` before ingest will run |
|
|
41
|
+
| **The database** | Postgres with **pgvector** — `CREATE EXTENSION vector;` | any managed host; the DDL below needs a role that can create tables |
|
|
42
|
+
| **The provider key** | `GEMINI_API_KEY` — the default embedding provider is `gemini-embedding-001`. Declaring `embedding.provider: openai` reads `OPENAI_API_KEY` instead: each provider names its own variable | [aistudio.google.com](https://aistudio.google.com/apikey); free tier — embedding input is free of charge |
|
|
43
|
+
| **The DSN** | `KSOR_DB_URL`, named by `instance.md`'s `database.dsn_env` | already named by `instance.md`'s `database:` block |
|
|
44
44
|
|
|
45
45
|
Both variables go in `.env` beside `instance.md` — `ksor` reads it automatically,
|
|
46
46
|
and `.env` is gitignored. Every command below is run **from your repository
|
|
@@ -197,8 +197,30 @@ measure until the corpus is in there.
|
|
|
197
197
|
pnpm exec ksor calibrate --instance instance.md
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
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:
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
pnpm exec ksor calibrate --instance instance.md --queries-file questions.txt
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Six to ten real questions is enough. They should be things this record answers,
|
|
216
|
+
in the words someone would actually ask — the floor is set by the WEAKEST of
|
|
217
|
+
them, so a vague question drags it down and a question the record does not
|
|
218
|
+
answer invalidates the measurement. The door is recorded beside the number
|
|
219
|
+
(`door: queries-file`), because floors from the two doors are measured against
|
|
220
|
+
different distributions and must never be compared as interchangeable.
|
|
221
|
+
|
|
222
|
+
Either way it ends with a block to paste into **`instance.md`**'s frontmatter,
|
|
223
|
+
exactly as printed — the floor, the measurement recorded beside it as a comment, and
|
|
202
224
|
`floor_digest`, the digest of the retrieval predicate the floor was measured
|
|
203
225
|
through. Paste it, then restart `ksor serve`: the floor is read at boot.
|
|
204
226
|
|
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) {
|
|
@@ -100,8 +100,11 @@ Stand it up in this order (each step's errors explain how to fix themselves):
|
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
That is enough. `embedding:` is optional and already defaults to
|
|
103
|
-
`provider: gemini`, `model: gemini-embedding-001`, `dim: 1536
|
|
104
|
-
|
|
103
|
+
`provider: gemini`, `model: gemini-embedding-001`, `dim: 1536`. The other
|
|
104
|
+
shipped vendor is OpenAI — `provider: openai`, `model:
|
|
105
|
+
text-embedding-3-small`, `dim: 1536`, key in `OPENAI_API_KEY` — and each
|
|
106
|
+
provider names its own key variable, so nothing else changes. Write the block
|
|
107
|
+
out only to pin the space explicitly or to change it — and note that model and
|
|
105
108
|
dim are the PERSISTED identity of the embedding space, so changing either
|
|
106
109
|
later means re-embedding the whole corpus. Keep `dim` at or below 2000 — the
|
|
107
110
|
schema indexes a `vector` column directly and pgvector's HNSW takes a
|
|
@@ -114,7 +117,41 @@ Stand it up in this order (each step's errors explain how to fix themselves):
|
|
|
114
117
|
Leave `retrieval:` out for now — the gate is off and the server says so.
|
|
115
118
|
Turning it on is step 4, AFTER the record is serving.
|
|
116
119
|
|
|
117
|
-
2. **
|
|
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. 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.
|
|
130
|
+
With it connected, ask:
|
|
131
|
+
|
|
132
|
+
> Using the Neon MCP server, create a project called `<your-record>` and
|
|
133
|
+
> enable the pgvector extension on it. Then create a branch called `dev`,
|
|
134
|
+
> and save that branch's connection string to `.env` as `KSOR_DB_URL`.
|
|
135
|
+
> Never print my API key. Show me the plan before you run anything.
|
|
136
|
+
|
|
137
|
+
That is `create_project`, `run_sql` (`CREATE EXTENSION vector`),
|
|
138
|
+
`create_branch` and `get_connection_string` — four real tools, no dashboard.
|
|
139
|
+
Prefer the OAuth flow: an API key pasted into `.mcp.json` would be committed,
|
|
140
|
+
because that file carries no secret and is not gitignored.
|
|
141
|
+
|
|
142
|
+
Any Postgres with pgvector works — Neon is the path with an MCP server, not a
|
|
143
|
+
requirement. Locally: `docker run -e POSTGRES_PASSWORD=x -p 5432:5432
|
|
144
|
+
pgvector/pgvector:pg17`.
|
|
145
|
+
|
|
146
|
+
**`GEMINI_API_KEY` is the one step no agent can do.** No vendor mints an API
|
|
147
|
+
key over a protocol; it comes from a browser at
|
|
148
|
+
[aistudio.google.com/apikey](https://aistudio.google.com/apikey) and a human
|
|
149
|
+
pastes it. An agent working through this list should ask for it by name and
|
|
150
|
+
stop, rather than trying — and should say that the FREE TIER is enough:
|
|
151
|
+
embedding input on `gemini-embedding-001` is free of charge, so this is a
|
|
152
|
+
signup, not a bill.
|
|
153
|
+
|
|
154
|
+
3. **Copy `.env.example` to `.env`** and fill it in — `ksor` reads it
|
|
118
155
|
automatically, so nothing needs exporting, and `.env` is already gitignored.
|
|
119
156
|
A real environment variable still wins over the file, so CI and production
|
|
120
157
|
overrides behave normally.
|
|
@@ -134,7 +171,7 @@ Stand it up in this order (each step's errors explain how to fix themselves):
|
|
|
134
171
|
intended dev shape. A PUBLIC deployment configures the SSO door instead —
|
|
135
172
|
see the comments in `.env.example` and "Serving safely" below.
|
|
136
173
|
|
|
137
|
-
|
|
174
|
+
4. **Bring it up.** Once, then every time:
|
|
138
175
|
|
|
139
176
|
```sh
|
|
140
177
|
pnpm provision # schema (or migrate) + grant — the privileged acts, run once
|
|
@@ -219,7 +256,7 @@ Stand it up in this order (each step's errors explain how to fix themselves):
|
|
|
219
256
|
serves, or a DBA who holds the credentials that authorize ingest — not as a
|
|
220
257
|
daily choice.
|
|
221
258
|
|
|
222
|
-
|
|
259
|
+
5. **Turn the abstention gate on — deliberately, once it serves.** This is the
|
|
223
260
|
step that makes "not in this corpus" a real answer, and it is measured, never
|
|
224
261
|
guessed:
|
|
225
262
|
|