@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/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as resolveCommand, r as verbs, t as exitCodes } from "./src-dqpI-p1a.mjs";
3
- import { A as parseViewer, B as tlsPosture, C as contentPoolMin, D as outlineHandler, E as keyRingFromEnv, F as runProbe$1, H as withPgRetry$1, I as searchHandler, L as servingPolicy$1, M as prewarmPool, N as readHandler, O as parseInstanceText$1, P as recordIsUndescribed, R as storedTextSearchConfig, S as contentPool$1, T as instancePathOf$1, U as withProbeDeadline$1, V as validateViewer, W as z$1, _ as assertGovernanceServable$1, a as GovernanceGateError$1, b as checkEmbeddingSpace$1, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, g as TextSearchConfigMismatch, h as TRUST_TIERS$1, i as FLOOR, j as pooledEndpointFor, k as parseTrustFloor, l as MissingProviderKeyError$1, m as SchemaVersionError, n as ContentStoreError$1, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, r as EmbeddingSpaceMismatch$1, t as AudienceError$1, u as OUTLINE_OUTPUT, v as assertSchemaCompatible, w as embedQueryVlit, x as composeInstructions, y as buildShippedProvider$1, z as tallyHandlers } from "./gateway-api-Ck1l_Sgs-B-_wMYLB.mjs";
3
+ import { A as parseViewer, B as tallyHandlers, C as contentPoolMin, D as outlineHandler, E as keyRingFromEnv, F as recordIsUndescribed, G as z$1, H as validateViewer, I as runProbe$1, L as searchHandler, M as prewarmPool, N as providerKeyEnv$1, O as parseInstanceText$1, P as readHandler, R as servingPolicy$1, S as contentPool$1, T as instancePathOf$1, U as withPgRetry$1, V as tlsPosture, W as withProbeDeadline$1, _ as assertGovernanceServable$1, a as GovernanceGateError$1, b as checkEmbeddingSpace$1, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, g as TextSearchConfigMismatch, h as TRUST_TIERS$1, i as FLOOR, j as pooledEndpointFor, k as parseTrustFloor, l as MissingProviderKeyError$1, m as SchemaVersionError, n as ContentStoreError$1, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, r as EmbeddingSpaceMismatch$1, t as AudienceError$1, u as OUTLINE_OUTPUT, v as assertSchemaCompatible, w as embedQueryVlit, x as composeInstructions, y as buildShippedProvider$1, z as storedTextSearchConfig } from "./gateway-api-C0vL3oOK-D24n786A.mjs";
4
4
  import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { InMemoryTransport, LATEST_PROTOCOL_VERSION, createMcpHandler } from "@modelcontextprotocol/server";
@@ -17,7 +17,7 @@ import { Document, YAMLParseError, isCollection, isMap, isPair, isSeq, parseAllD
17
17
  import { parseArgs } from "node:util";
18
18
  import { readFile } from "node:fs/promises";
19
19
  import { execFileSync, spawnSync } from "node:child_process";
20
- //#region ../content-gateway/dist/main-mQtKKwyj.mjs
20
+ //#region ../content-gateway/dist/main-BoKhKcP0.mjs
21
21
  /**
22
22
  * The default registration — and the ORIGINAL of the file `ksor init` emits.
23
23
  *
@@ -1008,7 +1008,7 @@ async function compose(rawInstancePath, version) {
1008
1008
  let provider;
1009
1009
  try {
1010
1010
  provider = buildShippedProvider$1(instance.embeddingProvider, {
1011
- apiKey: process.env["GEMINI_API_KEY"] ?? null,
1011
+ apiKey: process.env[providerKeyEnv$1(instance.embeddingProvider) ?? ""] ?? null,
1012
1012
  modelId: instance.embeddingModel,
1013
1013
  dim: instance.embeddingDim
1014
1014
  });
@@ -1533,7 +1533,7 @@ async function main$1(version = GATEWAY_VERSION) {
1533
1533
  }
1534
1534
  }
1535
1535
  //#endregion
1536
- //#region ../content/dist/check-DNWlQuBg.mjs
1536
+ //#region ../content/dist/check-6fB_kR4G.mjs
1537
1537
  /**
1538
1538
  * One reader for the control files beside the bundle (`.ksor/*.yaml`): the
1539
1539
  * same parser posture as a concept's frontmatter — one document, unique keys,
@@ -2825,6 +2825,7 @@ const LEDGER_HEADER$1 = "# The takedown ledger (record spec §5): append-only, w
2825
2825
  const CONTROL_FILES = [
2826
2826
  "instance.md",
2827
2827
  ".ksor/governance.yaml",
2828
+ ".ksor/people.yaml",
2828
2829
  ".ksor/takedowns.yaml"
2829
2830
  ];
2830
2831
  /** Files the operating system writes behind the author's back: ignored, never reported. */
@@ -4709,7 +4710,7 @@ async function withPgRetry(op, options = {}) {
4709
4710
  throw lastError;
4710
4711
  }
4711
4712
  //#endregion
4712
- //#region ../content/dist/commands-Cb63_n_A.mjs
4713
+ //#region ../content/dist/commands-CO9-bQnb.mjs
4713
4714
  /**
4714
4715
  * EVAL-LOCKED constants, quarried verbatim from the oracle
4715
4716
  * (sor-agentfactory @ b554f91, config.py) — changing any of these is a
@@ -5823,13 +5824,6 @@ const INGEST_RETRY = {
5823
5824
  initialS: 1,
5824
5825
  maxS: 30
5825
5826
  };
5826
- /** Fail-fast for the query path: 2 attempts, sub-second backoff, no 429 retry — worst case a
5827
- * query embed costs ~1 s before the caller degrades, never a 15 s stall on the read. */
5828
- const QUERY_RETRY = {
5829
- attempts: 2,
5830
- initialS: .2,
5831
- maxS: .5
5832
- };
5833
5827
  function sleepS(seconds) {
5834
5828
  return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
5835
5829
  }
@@ -5855,17 +5849,6 @@ async function embedIntent(texts, opts) {
5855
5849
  return contract(texts, await withRetry(INGEST_RETRY, (exc) => provider.isRetryable(exc), () => provider.reset(), () => provider.embed(texts, { intent })), provider.dim);
5856
5850
  }
5857
5851
  /**
5858
- * The READ plane's door (query-embed, intent "query"). Same contract;
5859
- * FAIL-FAST retry (no 429, 2 attempts): a rate limit degrades the search to
5860
- * keyword-only in under a second instead of stalling the read ~15 s. Async
5861
- * like everything here — the oracle's `a` prefix marked Python's async
5862
- * flavor and is kept only for greppability against the Python.
5863
- */
5864
- async function aembedIntent(texts, opts) {
5865
- const { provider, intent } = opts;
5866
- return contract(texts, await withRetry(QUERY_RETRY, (exc) => provider.isRetryableQuery(exc), () => provider.reset(), () => provider.embed(texts, { intent })), provider.dim);
5867
- }
5868
- /**
5869
5852
  * The deterministic, key-free fake provider — a KSOR ADDITION (no oracle
5870
5853
  * counterpart; decision 6 conversion note). It exists so the DB tier and CI
5871
5854
  * can exercise ingest + retrieval end to end without a vendor key: same text
@@ -5937,7 +5920,7 @@ var FakeEmbeddingProvider = class {
5937
5920
  }
5938
5921
  reset() {}
5939
5922
  };
5940
- const DEFAULT_BASE = "https://generativelanguage.googleapis.com/v1beta";
5923
+ const DEFAULT_BASE$1 = "https://generativelanguage.googleapis.com/v1beta";
5941
5924
  /**
5942
5925
  * An HTTP-shaped failure carrying the status the retry classifier reads.
5943
5926
  *
@@ -5955,7 +5938,7 @@ var GeminiHttpError = class extends Error {
5955
5938
  };
5956
5939
  /** One POST, with the key in a HEADER — never the query string, which is logged. */
5957
5940
  async function post(opts, apiKey, path, body, timeoutMs) {
5958
- const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl ?? DEFAULT_BASE}${path}`, {
5941
+ const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl ?? DEFAULT_BASE$1}${path}`, {
5959
5942
  method: "POST",
5960
5943
  headers: {
5961
5944
  "x-goog-api-key": apiKey,
@@ -6025,33 +6008,33 @@ function geminiRestTextClient(apiKey, opts = {}) {
6025
6008
  * Python-SDK failure mode with no JS equivalent; `reset()`
6026
6009
  * keeps its drop-never-close contract regardless.
6027
6010
  */
6028
- function httpStatusOf(exc) {
6011
+ function httpStatusOf$1(exc) {
6029
6012
  if (!(exc instanceof Error)) return void 0;
6030
6013
  const status = exc.status;
6031
6014
  return typeof status === "number" ? status : void 0;
6032
6015
  }
6033
- function isTransportBlip(exc, depth = 0) {
6016
+ function isTransportBlip$1(exc, depth = 0) {
6034
6017
  if (depth > 5 || !(exc instanceof Error)) return false;
6035
6018
  if (exc.name === "AbortError" || exc.name === "TimeoutError") return true;
6036
6019
  const code = exc.code;
6037
6020
  if (typeof code === "string" && /^E[A-Z0-9_]+$/.test(code)) return true;
6038
6021
  if (exc instanceof TypeError && exc.message.includes("fetch failed")) return true;
6039
- return isTransportBlip(exc.cause, depth + 1);
6022
+ return isTransportBlip$1(exc.cause, depth + 1);
6040
6023
  }
6041
6024
  /** The Gemini taxonomy for the INGEST plane: transport blips, 5xx, AND 429 —
6042
6025
  * batch work is resumable and has no user waiting. */
6043
- function isRetryable(exc) {
6044
- if (isTransportBlip(exc)) return true;
6045
- const status = httpStatusOf(exc);
6026
+ function isRetryable$1(exc) {
6027
+ if (isTransportBlip$1(exc)) return true;
6028
+ const status = httpStatusOf$1(exc);
6046
6029
  if (status === void 0) return false;
6047
6030
  return status >= 500 && status <= 599 || status === 429;
6048
6031
  }
6049
6032
  /** The READ path's predicate: transport blips + 5xx only — NEVER 429. A
6050
6033
  * rate-limited project stays rate-limited on the next second; the correct
6051
6034
  * move is to degrade to keyword-only NOW, not stall the read behind backoff. */
6052
- function isRetryableQuery(exc) {
6053
- if (isTransportBlip(exc)) return true;
6054
- const status = httpStatusOf(exc);
6035
+ function isRetryableQuery$1(exc) {
6036
+ if (isTransportBlip$1(exc)) return true;
6037
+ const status = httpStatusOf$1(exc);
6055
6038
  return status !== void 0 && status >= 500 && status <= 599;
6056
6039
  }
6057
6040
  /**
@@ -6102,10 +6085,10 @@ var GeminiEmbeddingProvider = class {
6102
6085
  })).embeddings ?? []).map((e) => [...e.values ?? []]);
6103
6086
  }
6104
6087
  isRetryable(exc) {
6105
- return isRetryable(exc);
6088
+ return isRetryable$1(exc);
6106
6089
  }
6107
6090
  isRetryableQuery(exc) {
6108
- return isRetryableQuery(exc);
6091
+ return isRetryableQuery$1(exc);
6109
6092
  }
6110
6093
  };
6111
6094
  /**
@@ -6118,7 +6101,7 @@ var GeminiTextGenerator = class {
6118
6101
  clientFactory;
6119
6102
  client = null;
6120
6103
  constructor(opts) {
6121
- this.model = opts.model ?? "gemini-2.5-flash";
6104
+ this.model = opts.model ?? "gemini-3.7-flash";
6122
6105
  this.clientFactory = opts.clientFactory ?? (() => geminiRestTextClient(opts.apiKey));
6123
6106
  }
6124
6107
  getClient() {
@@ -6139,9 +6122,174 @@ var GeminiTextGenerator = class {
6139
6122
  }
6140
6123
  })).text ?? "";
6141
6124
  }
6125
+ isRetryable(exc) {
6126
+ return isRetryable$1(exc);
6127
+ }
6128
+ };
6129
+ /**
6130
+ * OpenAI's embedding endpoint, over `fetch` — no SDK.
6131
+ *
6132
+ * The same call decision 12's 2026-08-22 revision made for Gemini, for the same
6133
+ * reason: this is ONE HTTP call behind a structurally-typed slice, and a vendor
6134
+ * SDK would put megabytes and a transitive dependency tree into every
6135
+ * `ksor init` that never embeds anything. If a provider ever needs an SDK, the
6136
+ * seam takes one through `clientFactory`; nothing here forecloses that.
6137
+ *
6138
+ * WHAT DIFFERS FROM GEMINI, and it is worth knowing before choosing:
6139
+ *
6140
+ * Gemini embeds ASYMMETRICALLY — `RETRIEVAL_DOCUMENT` and `RETRIEVAL_QUERY`
6141
+ * produce different vectors for the same text, and the plane's intent picks
6142
+ * which. OpenAI has no task type: a query and a document are embedded
6143
+ * identically. The seam anticipates exactly this — "a provider whose two
6144
+ * vendor labels are equal can never mis-route a plane"
6145
+ * (`lib/embedding.ts`) — so both labels are the empty string and the intent
6146
+ * reaches this transport and is deliberately ignored.
6147
+ *
6148
+ * `dimensions` is supported on `text-embedding-3-*` only. Asking an older
6149
+ * model for a dimension is an error there, not a silent full-width vector,
6150
+ * which is the failure the framework's own width check would catch anyway.
6151
+ */
6152
+ /**
6153
+ * An HTTP-shaped failure carrying the status the retry classifier reads — and
6154
+ * the vendor's own error `type`, which the status alone does not distinguish.
6155
+ *
6156
+ * OpenAI answers a spent balance with **429**, the same status as a rate limit:
6157
+ * `{"error":{"type":"insufficient_quota","code":"credit_balance_exhausted"}}`
6158
+ * (observed live, 2026-09-01). One clears by waiting and one never will, so a
6159
+ * classifier reading only the status retries a billing problem five times with
6160
+ * exponential backoff and then reports it — slowly, and as if it had been
6161
+ * transient.
6162
+ */
6163
+ var OpenAiHttpError = class extends Error {
6164
+ status;
6165
+ /** The vendor's `error.type`, when it sends one. */
6166
+ kind;
6167
+ constructor(status, detail, kind = null) {
6168
+ super(`OpenAI API error ${status}: ${detail}`);
6169
+ this.name = "OpenAiHttpError";
6170
+ this.status = status;
6171
+ this.kind = kind;
6172
+ }
6173
+ };
6174
+ const DEFAULT_BASE = "https://api.openai.com/v1";
6175
+ function openAiRestEmbedClient(apiKey, opts = {}) {
6176
+ return { async embed(params) {
6177
+ const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl ?? DEFAULT_BASE}/embeddings`, {
6178
+ method: "POST",
6179
+ headers: {
6180
+ authorization: `Bearer ${apiKey}`,
6181
+ "content-type": "application/json"
6182
+ },
6183
+ body: JSON.stringify({
6184
+ model: params.model,
6185
+ input: [...params.input],
6186
+ dimensions: params.dimensions
6187
+ }),
6188
+ signal: AbortSignal.timeout(params.timeoutMs)
6189
+ });
6190
+ const text = await res.text();
6191
+ if (!res.ok) {
6192
+ let detail = text.slice(0, 300);
6193
+ let kind = null;
6194
+ try {
6195
+ const err = JSON.parse(text).error;
6196
+ if (typeof err?.message === "string") detail = err.message;
6197
+ if (typeof err?.type === "string") kind = err.type;
6198
+ } catch {}
6199
+ throw new OpenAiHttpError(res.status, detail, kind);
6200
+ }
6201
+ const data = [...JSON.parse(text).data ?? []];
6202
+ data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
6203
+ return { embeddings: data.map((d) => ({ values: d.embedding })) };
6204
+ } };
6205
+ }
6206
+ /** True for a transport blip with no HTTP status of its own. */
6207
+ function isTransportBlip(exc) {
6208
+ if (exc instanceof OpenAiHttpError) return false;
6209
+ const name = exc?.name;
6210
+ return name === "AbortError" || name === "TimeoutError" || name === "TypeError";
6211
+ }
6212
+ function httpStatusOf(exc) {
6213
+ return exc instanceof OpenAiHttpError ? exc.status : void 0;
6214
+ }
6215
+ /**
6216
+ * The INGEST plane's taxonomy: transport blips, 5xx, AND 429 — batch work is
6217
+ * resumable and has nobody waiting. Deliberately the same shape as the Gemini
6218
+ * adapter's, because the two planes are a property of ksor, not of a vendor.
6219
+ */
6220
+ function isRetryable(exc) {
6221
+ if (isTransportBlip(exc)) return true;
6222
+ if (exc instanceof OpenAiHttpError && exc.kind === "insufficient_quota") return false;
6223
+ const status = httpStatusOf(exc);
6224
+ if (status === void 0) return false;
6225
+ return status >= 500 && status <= 599 || status === 429;
6226
+ }
6227
+ /**
6228
+ * The READ plane's: transport blips + 5xx only, NEVER 429. A rate-limited
6229
+ * project stays rate-limited on the next second, so a search degrades to
6230
+ * keyword-only now rather than stalling a reader behind backoff.
6231
+ */
6232
+ /**
6233
+ * An ACCOUNT-level failure: no amount of waiting and no other passage changes
6234
+ * it. The drain must abort on this rather than quarantine, because the chunk
6235
+ * it happened to be holding is not what is wrong — see `ingest/worker.ts`.
6236
+ */
6237
+ function isFatal(exc) {
6238
+ return exc instanceof OpenAiHttpError && exc.kind === "insufficient_quota";
6239
+ }
6240
+ function isRetryableQuery(exc) {
6241
+ if (isTransportBlip(exc)) return true;
6242
+ const status = httpStatusOf(exc);
6243
+ return status !== void 0 && status >= 500 && status <= 599;
6244
+ }
6245
+ var OpenAiEmbeddingProvider = class {
6246
+ providerId = "openai";
6247
+ modelId;
6248
+ dim;
6249
+ documentTaskLabel;
6250
+ queryTaskLabel;
6251
+ documentTimeoutMs;
6252
+ queryTimeoutMs;
6253
+ clientFactory;
6254
+ client = null;
6255
+ constructor(opts) {
6256
+ this.modelId = opts.modelId;
6257
+ this.dim = opts.dim;
6258
+ this.documentTaskLabel = opts.documentTaskLabel;
6259
+ this.queryTaskLabel = opts.queryTaskLabel;
6260
+ this.documentTimeoutMs = Math.trunc(opts.documentTimeoutS * 1e3);
6261
+ this.queryTimeoutMs = Math.trunc(opts.queryTimeoutS * 1e3);
6262
+ this.clientFactory = opts.clientFactory ?? (() => openAiRestEmbedClient(opts.apiKey));
6263
+ }
6264
+ get recipe() {
6265
+ return `${this.modelId}/d${this.dim}/${this.documentTaskLabel}`;
6266
+ }
6267
+ getClient() {
6268
+ this.client ??= this.clientFactory();
6269
+ return this.client;
6270
+ }
6271
+ /** DROP the client reference, never close it: in-flight calls finish on their
6272
+ * own reference and the next call rebuilds lazily. Idempotent. */
6273
+ reset() {
6274
+ this.client = null;
6275
+ }
6276
+ async embed(texts, opts) {
6277
+ return (await this.getClient().embed({
6278
+ model: this.modelId,
6279
+ input: texts,
6280
+ dimensions: this.dim,
6281
+ timeoutMs: opts.intent === "document" ? this.documentTimeoutMs : this.queryTimeoutMs
6282
+ })).embeddings.map((e) => [...e.values ?? []]);
6283
+ }
6142
6284
  isRetryable(exc) {
6143
6285
  return isRetryable(exc);
6144
6286
  }
6287
+ isRetryableQuery(exc) {
6288
+ return isRetryableQuery(exc);
6289
+ }
6290
+ isFatal(exc) {
6291
+ return isFatal(exc);
6292
+ }
6145
6293
  };
6146
6294
  /**
6147
6295
  * The embedding-provider registry — a plain object, NOT any discovery
@@ -6168,20 +6316,49 @@ var GeminiTextGenerator = class {
6168
6316
  */
6169
6317
  var MissingProviderKeyError = class extends Error {
6170
6318
  providerName;
6171
- constructor(providerName) {
6172
- super(`embedding provider ${JSON.stringify(providerName)} needs an API key and none was supplied`);
6319
+ keyEnv;
6320
+ /**
6321
+ * `keyEnv` is not decoration. The message named the PROVIDER and nothing
6322
+ * else, so an operator whose `ksor serve` exited 3 on an OpenAI record was
6323
+ * told "provider openai needs an API key" and left to guess which variable —
6324
+ * while `ksor serve --help`, `env.example` and `docs/deploying.md` all named
6325
+ * `GEMINI_API_KEY`, which the door does not read (review, 2026-09-01). The
6326
+ * registry row already held the answer; this is it reaching the operator.
6327
+ */
6328
+ constructor(providerName, keyEnv = null) {
6329
+ super(`embedding provider ${JSON.stringify(providerName)} needs an API key and none was supplied` + (keyEnv === null ? "" : ` — set ${keyEnv}`));
6173
6330
  this.name = "MissingProviderKeyError";
6174
6331
  this.providerName = providerName;
6332
+ this.keyEnv = keyEnv;
6175
6333
  }
6176
6334
  };
6177
6335
  const PROVIDERS = {
6178
6336
  gemini: {
6179
6337
  build: (opts) => new GeminiEmbeddingProvider(opts),
6180
- needsApiKey: true
6338
+ needsApiKey: true,
6339
+ keyEnv: "GEMINI_API_KEY",
6340
+ taskLabels: {
6341
+ document: EMBED_TASK_DOCUMENT,
6342
+ query: EMBED_TASK_QUERY
6343
+ }
6344
+ },
6345
+ openai: {
6346
+ build: (opts) => new OpenAiEmbeddingProvider(opts),
6347
+ needsApiKey: true,
6348
+ keyEnv: "OPENAI_API_KEY",
6349
+ taskLabels: {
6350
+ document: "",
6351
+ query: ""
6352
+ }
6181
6353
  },
6182
6354
  fake: {
6183
6355
  build: (opts) => new FakeEmbeddingProvider(opts),
6184
- needsApiKey: false
6356
+ needsApiKey: false,
6357
+ keyEnv: null,
6358
+ taskLabels: {
6359
+ document: EMBED_TASK_DOCUMENT,
6360
+ query: EMBED_TASK_QUERY
6361
+ }
6185
6362
  }
6186
6363
  };
6187
6364
  function entryFor(name) {
@@ -6196,6 +6373,13 @@ function providerNeedsApiKey(name) {
6196
6373
  return entryFor(name).needsApiKey;
6197
6374
  }
6198
6375
  /**
6376
+ * The environment variable this provider's key comes from, or null when it
6377
+ * needs none. Unknown name → the same loud error as building it.
6378
+ */
6379
+ function providerKeyEnv(name) {
6380
+ return entryFor(name).keyEnv;
6381
+ }
6382
+ /**
6199
6383
  * The port door: the named provider bound to the DECLARED embedding space and
6200
6384
  * the framework's timeout knobs. `modelId`/`dim` omitted = the shipped
6201
6385
  * config space (EMBED_MODEL / EMBED_DIM, eval-locked); an instance may
@@ -6207,13 +6391,13 @@ function providerNeedsApiKey(name) {
6207
6391
  */
6208
6392
  function buildShippedProvider(name, opts) {
6209
6393
  const entry = entryFor(name);
6210
- if (entry.needsApiKey && !opts.apiKey) throw new MissingProviderKeyError(name);
6394
+ if (entry.needsApiKey && !opts.apiKey) throw new MissingProviderKeyError(name, entry.keyEnv);
6211
6395
  return entry.build({
6212
6396
  apiKey: opts.apiKey ?? "",
6213
6397
  modelId: opts.modelId ?? "gemini-embedding-001",
6214
6398
  dim: opts.dim ?? 1536,
6215
- documentTaskLabel: EMBED_TASK_DOCUMENT,
6216
- queryTaskLabel: EMBED_TASK_QUERY,
6399
+ documentTaskLabel: entry.taskLabels.document,
6400
+ queryTaskLabel: entry.taskLabels.query,
6217
6401
  documentTimeoutS: EMBED_TIMEOUT_S(),
6218
6402
  queryTimeoutS: QUERY_EMBED_TIMEOUT_S()
6219
6403
  });
@@ -6711,7 +6895,7 @@ function calibrationScope(viewer) {
6711
6895
  async function scoreQueries(pool, scope, gucs, provider, queries, inCorpus) {
6712
6896
  const out = [];
6713
6897
  for (const query of queries) {
6714
- const [vector] = await aembedIntent([query], {
6898
+ const [vector] = await embedIntent([query], {
6715
6899
  provider,
6716
6900
  intent: "query"
6717
6901
  });
@@ -9113,6 +9297,7 @@ async function drain(pending, io) {
9113
9297
  literals = await io.embedBatch(batch.map(([, text]) => text));
9114
9298
  } catch (exc) {
9115
9299
  if (io.isRetryable(exc)) throw exc;
9300
+ if (io.isFatal?.(exc) === true) throw exc;
9116
9301
  if (batch.length === 1) {
9117
9302
  await io.markFailed(failureReason(exc), batch[0][0]);
9118
9303
  failed += 1;
@@ -9664,7 +9849,8 @@ async function buildGeneration(pool, instance, options) {
9664
9849
  markFailed: (reason, chunkId) => runIngest(pool, tenant, async (c) => {
9665
9850
  await c.query(FAIL_SQL, [reason, chunkId]);
9666
9851
  }),
9667
- isRetryable: (exc) => provider.isRetryable(exc)
9852
+ isRetryable: (exc) => provider.isRetryable(exc),
9853
+ isFatal: (exc) => provider.isFatal?.(exc) === true
9668
9854
  });
9669
9855
  log(`embedded ${embedded}, failed ${failed}`);
9670
9856
  const fin = await runIngest(pool, tenant, async (c) => {
@@ -9743,6 +9929,43 @@ function pct(fraction) {
9743
9929
  return `${(fraction * 100).toFixed(0)}%`;
9744
9930
  }
9745
9931
  /**
9932
+ * What to do when calibration is refused for quota — named, not left as the
9933
+ * vendor's sentence.
9934
+ *
9935
+ * Both of these were hit walking a real free-tier key, and they are DIFFERENT
9936
+ * failures with different remedies, which is why the raw error is not enough:
9937
+ *
9938
+ * generate_content …/gemini-3.7-flash, limit 5 the SYNTHESIZED door writes
9939
+ * one probe question per
9940
+ * sampled passage, and a free
9941
+ * key allows a handful of
9942
+ * generations a minute. No
9943
+ * amount of waiting fixes a
9944
+ * corpus of any size; the
9945
+ * answer is the zero-LLM door.
9946
+ * global_embed_content_requests_per_minute the EMBEDDING endpoint, which
9947
+ * both doors use. Transient:
9948
+ * an ingest immediately before
9949
+ * a calibration spends the same
9950
+ * per-minute budget.
9951
+ *
9952
+ * Product principle 4: a failure states what is wrong, why the rule exists, and
9953
+ * how to fix it. The vendor's message states only the first.
9954
+ */
9955
+ /** The generation quota, which no wait resolves on a free key. */
9956
+ const GENERATION = /generate_content|generativelanguage\.googleapis\.com\/generate/i;
9957
+ /** The embedding quota, which clears on its own. */
9958
+ const EMBEDDING = /embed_content|global_embed/i;
9959
+ /**
9960
+ * The remedy for a quota refusal, or null when the failure is not one this
9961
+ * knows — in which case the vendor's own message stands, unembellished.
9962
+ */
9963
+ function quotaRemedy(message) {
9964
+ if (GENERATION.test(message)) return "the SYNTHESIZED door writes one probe question per sampled passage with an LLM, and a free-tier key allows only a few generations a minute — a bigger corpus makes this worse, not better.\n fix: calibrate with zero LLM — write your in-corpus questions one per line and pass\n --queries-file PATH. The floor is measured the same way; only the questions differ,\n and the door is recorded beside the number so the two are never compared.";
9965
+ if (EMBEDDING.test(message)) return "the EMBEDDING endpoint is limited per minute, and both doors use it — an ingest immediately before this spends the same budget.\n fix: wait about a minute and run it again. Nothing was written; calibration only reads.";
9966
+ return null;
9967
+ }
9968
+ /**
9746
9969
  * How close to the floor an answer has to be to count as marginal.
9747
9970
  *
9748
9971
  * 0.01 is not a tuned threshold — it is the size of the decision in this
@@ -10175,8 +10398,9 @@ function composeProvider(instance) {
10175
10398
  try {
10176
10399
  let apiKey = null;
10177
10400
  if (providerNeedsApiKey(instance.embeddingProvider)) {
10178
- apiKey = process.env["GEMINI_API_KEY"] || null;
10179
- if (apiKey === null) return fail$1(ENVIRONMENT, "GEMINI_API_KEY is required (the instance's embedding provider needs a key)\n fix: export GEMINI_API_KEY=... and rerun");
10401
+ const keyEnv = providerKeyEnv(instance.embeddingProvider) ?? "";
10402
+ apiKey = process.env[keyEnv] || null;
10403
+ if (apiKey === null) return fail$1(ENVIRONMENT, `${keyEnv} is required (the instance's embedding provider ${JSON.stringify(instance.embeddingProvider)} needs a key)\n fix: export ${keyEnv}=... and rerun`);
10180
10404
  }
10181
10405
  return buildShippedProvider(instance.embeddingProvider, {
10182
10406
  apiKey,
@@ -10433,11 +10657,11 @@ async function calibrateCommand(args) {
10433
10657
  let textGenerator = null;
10434
10658
  if (queries === null) {
10435
10659
  const apiKey = process.env["GEMINI_API_KEY"];
10436
- if (apiKey === void 0 || apiKey === "") return refuse$2("bad-args", "the synthesized door needs GEMINI_API_KEY (it writes one probe question per sampled passage) — or calibrate with zero LLM: --queries-file PATH (one in-corpus question per line)");
10660
+ if (apiKey === void 0 || apiKey === "") return refuse$2("bad-args", "the synthesized door needs GEMINI_API_KEY (it writes one probe question per sampled passage) — or calibrate with zero LLM: --queries-file PATH (one in-corpus question per line).\n note: this is the TEXT generator, not the embedding provider. A record on `embedding.provider: openai` still embeds with OPENAI_API_KEY; only question synthesis is Gemini-only today, and --queries-file avoids it entirely");
10437
10661
  textGenerator = new GeminiTextGenerator({ apiKey });
10438
10662
  }
10439
10663
  const ooc = values["ooc-file"] === void 0 ? null : parseQueriesFile(readFileSync(values["ooc-file"], "utf8"));
10440
- const report = await withPool(dsn, async (pool) => runCalibration(pool, {
10664
+ const report = await withQuotaRemedy(async () => withPool(dsn, async (pool) => runCalibration(pool, {
10441
10665
  tenantId: instance.tenantId,
10442
10666
  corpusId: instance.corpusId,
10443
10667
  viewer: await widestViewer(pool, instance),
@@ -10448,12 +10672,31 @@ async function calibrateCommand(args) {
10448
10672
  oocProbes: ooc,
10449
10673
  perNode: values["per-node"] === void 0 ? void 0 : intFlag("--per-node", values["per-node"]),
10450
10674
  minChars: values["min-chars"] === void 0 ? void 0 : intFlag("--min-chars", values["min-chars"])
10451
- }));
10675
+ })));
10452
10676
  process.stdout.write(renderReport(report, GATE_PREDICATE_DIGEST) + "\n");
10453
10677
  const advice = overlapAdvice(report);
10454
10678
  if (advice !== null) process.stdout.write(advice);
10455
10679
  return 0;
10456
10680
  }
10681
+ /**
10682
+ * Run a calibration, turning a quota refusal into the remedy for THAT quota.
10683
+ *
10684
+ * Both failures reach here as the vendor's own sentence, which states what is
10685
+ * wrong and neither why nor how to fix it — and the two need opposite answers
10686
+ * (change door vs wait a minute). Anything `quotaRemedy` does not recognise is
10687
+ * re-thrown untouched: inventing advice for an error nobody has read is worse
10688
+ * than passing the vendor's through.
10689
+ */
10690
+ async function withQuotaRemedy(op) {
10691
+ try {
10692
+ return await op();
10693
+ } catch (exc) {
10694
+ const message = exc instanceof Error ? exc.message : String(exc);
10695
+ const remedy = quotaRemedy(message);
10696
+ if (remedy === null) throw exc;
10697
+ throw Object.assign(/* @__PURE__ */ new Error(`${message}\n why: ${remedy}`), { slug: "ksor-calibrate-quota" });
10698
+ }
10699
+ }
10457
10700
  /** How many days of traffic one --check reads. Bounded so a busy record cannot make it expensive. */
10458
10701
  const DRIFT_DEFAULT_DAYS = 30;
10459
10702
  /**
@@ -10796,7 +11039,7 @@ async function runContentCli(argv) {
10796
11039
  }
10797
11040
  }
10798
11041
  //#endregion
10799
- //#region ../content/dist/record-DnMnZelb.mjs
11042
+ //#region ../content/dist/record-Cxw0SUG3.mjs
10800
11043
  /**
10801
11044
  * May `surface` publish `doc` at instant `at`? `drafts` is the build's drafts switch,
10802
11045
  * which admits drafts to HUMAN surfaces only.
@@ -10842,6 +11085,7 @@ const lockSchema = z.object({
10842
11085
  drafts: z.enum(["hidden", "shown"]),
10843
11086
  instance_sha256: hex64,
10844
11087
  policy_sha256: hex64,
11088
+ people_sha256: hex64,
10845
11089
  ledger_sha256: hex64,
10846
11090
  ledger_entries: z.array(z.object({
10847
11091
  id: z.string().min(1),
@@ -10914,6 +11158,7 @@ function buildIdOf(inputs) {
10914
11158
  indexes: [...inputs.indexes].map((i) => [i.path, i.sha256]).sort((a, b) => compare(a[0] ?? "", b[0] ?? "")),
10915
11159
  instance_sha256: inputs.instance_sha256,
10916
11160
  policy_sha256: inputs.policy_sha256,
11161
+ people_sha256: inputs.people_sha256,
10917
11162
  ledger_sha256: inputs.ledger_sha256,
10918
11163
  ksor_version: inputs.ksor_version,
10919
11164
  drafts: inputs.drafts
@@ -10960,6 +11205,7 @@ function composeLock(input) {
10960
11205
  }));
10961
11206
  const instance_sha256 = sha256Hex(input.instanceText);
10962
11207
  const policy_sha256 = sha256Hex(input.policyText);
11208
+ const people_sha256 = sha256Hex(input.peopleText ?? "");
10963
11209
  const ledger_sha256 = sha256Hex(input.ledgerText ?? "");
10964
11210
  return {
10965
11211
  format: 1,
@@ -10970,6 +11216,7 @@ function composeLock(input) {
10970
11216
  indexes,
10971
11217
  instance_sha256,
10972
11218
  policy_sha256,
11219
+ people_sha256,
10973
11220
  ledger_sha256,
10974
11221
  ksor_version: input.ksorVersion,
10975
11222
  drafts: input.drafts
@@ -10982,6 +11229,7 @@ function composeLock(input) {
10982
11229
  drafts: input.drafts,
10983
11230
  instance_sha256,
10984
11231
  policy_sha256,
11232
+ people_sha256,
10985
11233
  ledger_sha256,
10986
11234
  ledger_entries: [...input.ledgerEntries].sort((a, b) => compare(a.id, b.id)),
10987
11235
  audiences: {
@@ -11144,6 +11392,7 @@ const INPUTS = [
11144
11392
  "knowledge",
11145
11393
  "instance.md",
11146
11394
  ".ksor/governance.yaml",
11395
+ ".ksor/people.yaml",
11147
11396
  ".ksor/takedowns.yaml"
11148
11397
  ];
11149
11398
  /**
@@ -11154,7 +11403,11 @@ const INPUTS = [
11154
11403
  * clone is invisible to every surface, so the build says so by name.
11155
11404
  */
11156
11405
  function ignoredGovernance(root) {
11157
- const paths = [".ksor/governance.yaml", ".ksor/takedowns.yaml"].filter((rel) => existsSync(join(root, rel)));
11406
+ const paths = [
11407
+ ".ksor/governance.yaml",
11408
+ ".ksor/people.yaml",
11409
+ ".ksor/takedowns.yaml"
11410
+ ].filter((rel) => existsSync(join(root, rel)));
11158
11411
  if (paths.length === 0) return [];
11159
11412
  const out = git(root, [
11160
11413
  "check-ignore",
@@ -11344,8 +11597,8 @@ function runBuild(args, cwd, io, options) {
11344
11597
  const facts = gitFacts(root);
11345
11598
  if (facts.repository && facts.historicLedger === null && !parsed.allowUnverifiable) return refuse$1(io, "ksor-ledger-unverifiable", facts.historyUnreadable === "shallow" ? "this is a shallow clone: the takedown ledger is append-only, and without history a deleted entry cannot be told from one that never existed" : "git could not read the takedown ledger's history (`git log -- .ksor/takedowns.yaml` failed, and this is not a shallow clone): the ledger is append-only, and without history a deleted entry cannot be told from one that never existed", facts.historyUnreadable === "shallow" ? "fetch full history (`git fetch --unshallow`; in CI, `fetch-depth: 0`), or pass --allow-unverifiable-ledger to build anyway" : "check that `git log` works in this checkout, or pass --allow-unverifiable-ledger to build anyway");
11346
11599
  const ignored = facts.repository ? ignoredGovernance(root) : [];
11347
- if (ignored.length > 0) return refuse$1(io, "ksor-governance-ignored", `git ignores ${ignored.join(" and ")}, so ${ignored.length === 1 ? "it is" : "they are"} in no commit — the policy and the takedown ledger ARE the record, and a clone (your CI, your deploy) would build without ${ignored.length === 1 ? "it" : "them"}`, "un-ignore them in .gitignore — the directory form `.ksor/` cannot be negated, so use `.ksor/*` plus `!.ksor/governance.yaml` and `!.ksor/takedowns.yaml` — then commit them (`ksor migrate` offers that edit)");
11348
- if (parsed.strict && facts.dirty) return refuse$1(io, "ksor-build-dirty", facts.repository ? "an input (knowledge/, instance.md, .ksor/governance.yaml, .ksor/takedowns.yaml) differs from its last commit, and --strict stamps only committed content" : "the record is not in a git repository, so no input is committed", "commit the inputs and rebuild, or drop --strict to stamp a dirty build (the lock says `dirty: true`)");
11600
+ if (ignored.length > 0) return refuse$1(io, "ksor-governance-ignored", `git ignores ${ignored.join(" and ")}, so ${ignored.length === 1 ? "it is" : "they are"} in no commit — the policy and the takedown ledger ARE the record, and a clone (your CI, your deploy) would build without ${ignored.length === 1 ? "it" : "them"}`, "un-ignore them in .gitignore — the directory form `.ksor/` cannot be negated, so use `.ksor/*` plus `!.ksor/governance.yaml`, `!.ksor/people.yaml` and `!.ksor/takedowns.yaml` — then commit them (`ksor migrate` offers that edit)");
11601
+ if (parsed.strict && facts.dirty) return refuse$1(io, "ksor-build-dirty", facts.repository ? "an input (knowledge/, instance.md, .ksor/governance.yaml, .ksor/people.yaml, .ksor/takedowns.yaml) differs from its last commit, and --strict stamps only committed content" : "the record is not in a git repository, so no input is committed", "commit the inputs and rebuild, or drop --strict to stamp a dirty build (the lock says `dirty: true`)");
11349
11602
  const baselines = [];
11350
11603
  if (facts.historicLedger !== null) baselines.push({
11351
11604
  source: "git history",
@@ -11386,6 +11639,7 @@ function runBuild(args, cwd, io, options) {
11386
11639
  drafts: options.drafts,
11387
11640
  instanceText: record.files.get("instance.md") ?? "",
11388
11641
  policyText: record.files.get(".ksor/governance.yaml") ?? "",
11642
+ peopleText: record.files.get(".ksor/people.yaml") ?? null,
11389
11643
  ledgerText,
11390
11644
  ledgerEntries: result.ledgerEntries,
11391
11645
  audiences: result.policy?.audiences ?? [],
@@ -13072,11 +13326,13 @@ const BARE_DOTKSOR_PATTERNS = /* @__PURE__ */ new Set([
13072
13326
  ]);
13073
13327
  const GOVERNANCE_IGNORE_BLOCK = [
13074
13328
  "# ksor's working directory — build output and scratch, never the record.",
13075
- "# The two governance files inside it ARE the record (the policy and the",
13076
- "# takedown ledger) and are un-ignored by name: the directory form `.ksor/`",
13077
- "# cannot be negated, so the glob is `.ksor/*`.",
13329
+ "# The governance files inside it ARE the record (the policy, the takedown",
13330
+ "# ledger, and the phone book the site publishes names from) and are",
13331
+ "# un-ignored by name: the directory form `.ksor/` cannot be negated, so the",
13332
+ "# glob is `.ksor/*`.",
13078
13333
  ".ksor/*",
13079
13334
  "!.ksor/governance.yaml",
13335
+ "!.ksor/people.yaml",
13080
13336
  "!.ksor/takedowns.yaml"
13081
13337
  ];
13082
13338
  /**
@@ -13350,7 +13606,8 @@ function isEnvironmentError(value) {
13350
13606
  const EMITTED_NAMES = /* @__PURE__ */ new Map([
13351
13607
  ["gitignore", ".gitignore"],
13352
13608
  ["env.example", ".env.example"],
13353
- ["dockerignore", ".dockerignore"]
13609
+ ["dockerignore", ".dockerignore"],
13610
+ ["mcp.json", ".mcp.json"]
13354
13611
  ]);
13355
13612
  const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
13356
13613
  ".md",
@@ -13712,7 +13969,9 @@ serves nothing. Runs in this process and holds it; SIGTERM/SIGINT drains.
13712
13969
  Configured by environment — .env beside the record is read automatically:
13713
13970
 
13714
13971
  <database.dsn_env> the Postgres DSN, under the NAME instance.md gives
13715
- GEMINI_API_KEY iff the instance's embedding provider needs a key
13972
+ <provider key> iff the instance's embedding provider needs one:
13973
+ GEMINI_API_KEY for gemini, OPENAI_API_KEY for openai.
13974
+ The refusal names the variable your record needs
13716
13975
  KSOR_AUTH disabled-local (loopback dev) | disabled-public.
13717
13976
  Serve REFUSES to boot with neither this nor a
13718
13977
  configured SSO door — never open by accident