@panaversity/ksor 0.0.49 → 0.0.51

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 CHANGED
@@ -1,5 +1,91 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.51
4
+
5
+ ### Patch Changes
6
+
7
+ - 6478ca4: Test infrastructure only — nothing an adopter installs behaves differently.
8
+
9
+ Every database-tier suite now bootstraps its scratch database under a name
10
+ unique to the run (`ksor_<slug>_<base36 ms>_<6 hex>`) instead of a fixed one.
11
+ Fixed names meant two runs against one Postgres — a second `pnpm test:db`, a CI
12
+ matrix job, an agent running the tier alongside a person — dropped each other's
13
+ database `WITH (FORCE)` mid-test, which surfaced as a missing table or a short
14
+ row count and read as flakiness. A new reaper (`scripts/db-reaper.ts`, the
15
+ tier's globalSetup) drops what an interrupted run leaks, and guard rule 12 keeps
16
+ the naming from drifting back.
17
+
18
+ - c466d4b: The scaffold moves to Fumadocs `16.15.4` (`fumadocs-core`, `fumadocs-ui`) and
19
+ `fumadocs-mdx` `15.4.0`.
20
+
21
+ Maintenance, not a fix — no advisory pushed it, and `npm audit` was already
22
+ clean. It is taken now because the four behaviours the scaffold cites BY VERSION
23
+ were re-verified against the new bytes rather than assumed, and each holds:
24
+ `CalloutType` is still the same six values (`fumadocs-ui/dist/components/callout.d.ts`);
25
+ `resolveHref` still resolves only the `./` and `../` forms and returns everything
26
+ else untouched, which is why the record keeps its own resolver; `remark-code-tab`
27
+ still honours `tab-group` on the `CodeBlockTabs` branch only, which is why the
28
+ scaffold picks that branch; and the search engine is still ZBSearch, so the
29
+ `language` option stays absent. Those citations now name `16.15.4`.
30
+
31
+ `fumadocs-ui` pins `fumadocs-core` exactly, so the two always move together;
32
+ `fumadocs-mdx@15.4.0` requires `fumadocs-core ^16.15.3`, which is what makes this
33
+ one change rather than three. Nothing else moves with it — Fumadocs peers Next as
34
+ a range (`16.x.x`). The committed pnpm lockfile is regenerated to match.
35
+
36
+ - 69d57f2: `ksor migrate --write-site` no longer deletes dependencies the adopter added to
37
+ their site.
38
+
39
+ Every file under `system/site` is offered as a whole-file replacement, which is
40
+ right for the copied rule modules and wrong for `system/site/package.json` — a
41
+ register ksor and the adopter both write in. Copying it whole removed anything
42
+ they had added, inside the same hunk that carried a pin bump, so a project could
43
+ stop building on the release meant to fix it. It is now merged per section: the
44
+ entries ksor ships move to this release's versions, the adopter's own survive,
45
+ and an entry ksor no longer ships is left alone rather than deleted (the tool
46
+ cannot tell one it retired from one they added).
47
+
48
+ Adds `docs/upgrading.md`, which ships in the tarball: the four-step path, the
49
+ table of what migrate carries, the list of files it does not — so an adopter
50
+ knows what to diff by hand — and the refusals to expect.
51
+
52
+ - 4b077aa: The scaffold pins Next `16.3.3`, clearing three high-severity advisories a
53
+ fresh `npm install` reported (#207).
54
+
55
+ `next@16.2.9` pulled `sharp@0.34.5` and `postcss@8.4.31`; the advisories are
56
+ against those, not against anything the scaffold declares, so the bump that
57
+ fixes them is Next's own. Measured on a fresh scaffold from the published CLI:
58
+ `npm audit` goes from **3 high to 0**, the static build takes 41.8s and emits
59
+ its 22 pages, and `llms.txt` carries its 5 entries unchanged. `16.3.3` is not
60
+ semver-major and Fumadocs peers Next as a range (`16.x.x`), so nothing else
61
+ moves with it. The committed pnpm lockfile is regenerated to match — the half
62
+ that would otherwise break an adopter whose CI installs frozen.
63
+
64
+ An existing project takes both across with
65
+ `ksor migrate --write-site`, which offers every file of `system/site` this
66
+ release emits — the pin and the config among them. It prints the diff and
67
+ changes nothing without `--write`.
68
+
69
+ The scaffold's `next.config.mjs` also sets `agentRules: false`. From Next 16.3,
70
+ a `next dev` that detects a coding agent writes `AGENTS.md` and `CLAUDE.md` into
71
+ the Next project root — which here is `system/site`, where the record's own
72
+ hygiene rule refuses markdown (`ksor-site-holds-content`: the site renders the
73
+ record, it never holds it). Left on, an adopter's `pnpm dev` turned their own
74
+ `pnpm check` red without their touching anything.
75
+
76
+ ## 0.0.50
77
+
78
+ ### Patch Changes
79
+
80
+ - f27f947: The three embed tuning variables now take effect when set in `.env`.
81
+ `KSOR_EMBED_TIMEOUT_S`, `KSOR_QUERY_EMBED_TIMEOUT_S` and `KSOR_EMBED_CACHE_MAX`
82
+ were read once at module load — before the CLI applies `.env` in `main()` — so
83
+ a value set there was silently ignored and the default stood. An adopter who
84
+ set `KSOR_EMBED_CACHE_MAX` to fit a small runtime, for instance, still got the
85
+ ~250 MB default cache and could OOM in production with nothing pointing at why.
86
+ The reads now happen at use. Exported shell variables were unaffected and still
87
+ are.
88
+
3
89
  ## 0.0.49
4
90
 
5
91
  ### Patch Changes
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-8DkVCYYi-Bo0_Y1Y3.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-D8HlLys2-Ca8OnwLn.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-BXNvtHqd.mjs
20
+ //#region ../content-gateway/dist/main-DMCMO8ye.mjs
21
21
  /**
22
22
  * The default registration — and the ORIGINAL of the file `ksor init` emits.
23
23
  *
@@ -4709,7 +4709,7 @@ async function withPgRetry(op, options = {}) {
4709
4709
  throw lastError;
4710
4710
  }
4711
4711
  //#endregion
4712
- //#region ../content/dist/commands-C3D7buZN.mjs
4712
+ //#region ../content/dist/commands-BNxmBRjs.mjs
4713
4713
  /**
4714
4714
  * EVAL-LOCKED constants, quarried verbatim from the oracle
4715
4715
  * (sor-agentfactory @ b554f91, config.py) — changing any of these is a
@@ -5787,11 +5787,12 @@ async function topOneScore(client, scope, queryVector) {
5787
5787
  })).rows[0]?.[0];
5788
5788
  return raw === void 0 || raw === null ? null : toNumber(raw, "score");
5789
5789
  }
5790
- const EMBED_TIMEOUT_S = envFloat("KSOR_EMBED_TIMEOUT_S", 60, 1);
5790
+ /** The per-request HTTP timeout for a document (ingest/batch) embed. */
5791
+ const EMBED_TIMEOUT_S = () => envFloat("KSOR_EMBED_TIMEOUT_S", 60, 1);
5791
5792
  /** Oracle env var: SOR_QUERY_EMBED_TIMEOUT_S. Note: query-embed.ts reads the
5792
5793
  * SAME variable with a different default (5.0) as its hard wall clock — two
5793
5794
  * deliberate reads, carried from the oracle (embedding.py:62 vs query_embed.py:44). */
5794
- const QUERY_EMBED_TIMEOUT_S = envFloat("KSOR_QUERY_EMBED_TIMEOUT_S", 10, 1);
5795
+ const QUERY_EMBED_TIMEOUT_S = () => envFloat("KSOR_QUERY_EMBED_TIMEOUT_S", 10, 1);
5795
5796
  /** The text we embed for a chunk: the readable hierarchical heading path, then the content. */
5796
5797
  function embedInput(title, headingPath, content) {
5797
5798
  const path = headingPath ? (title ? title + " > " : "") + headingPath.replaceAll("/", " > ").replaceAll("-", " ").trim() : title;
@@ -6213,8 +6214,8 @@ function buildShippedProvider(name, opts) {
6213
6214
  dim: opts.dim ?? 1536,
6214
6215
  documentTaskLabel: EMBED_TASK_DOCUMENT,
6215
6216
  queryTaskLabel: EMBED_TASK_QUERY,
6216
- documentTimeoutS: EMBED_TIMEOUT_S,
6217
- queryTimeoutS: QUERY_EMBED_TIMEOUT_S
6217
+ documentTimeoutS: EMBED_TIMEOUT_S(),
6218
+ queryTimeoutS: QUERY_EMBED_TIMEOUT_S()
6218
6219
  });
6219
6220
  }
6220
6221
  /** Written to yield ZERO ROWS, never an error, when the schema is absent. */
@@ -13058,9 +13059,10 @@ function siteChanges(root, templatesDir, stamps) {
13058
13059
  continue;
13059
13060
  }
13060
13061
  if (!SITE_TEXT_EXTENSIONS.has(path.extname(entry.name))) continue;
13061
- const after = applyProse(readFileSync(abs, "utf8").replaceAll("KSOR-STAMP-NAME", stamps.name).replaceAll("KSOR-STAMP-VERSION", stamps.version), manager);
13062
+ const rendered = applyProse(readFileSync(abs, "utf8").replaceAll("KSOR-STAMP-NAME", stamps.name).replaceAll("KSOR-STAMP-VERSION", stamps.version), manager);
13062
13063
  const target = path.join(root, child);
13063
13064
  const before = existsSync(target) ? readFileSync(target, "utf8") : null;
13065
+ const after = child === "system/site/package.json" && before !== null ? mergeSiteManifest(before, rendered) : rendered;
13064
13066
  if (before !== after) out.push({
13065
13067
  path: child,
13066
13068
  before,
@@ -13072,6 +13074,61 @@ function siteChanges(root, templatesDir, stamps) {
13072
13074
  return out;
13073
13075
  }
13074
13076
  /**
13077
+ * The site manifest, merged: what ksor pins wins, what the adopter added stays.
13078
+ *
13079
+ * Applied per SECTION rather than per file. `dependencies`, `devDependencies`
13080
+ * and `scripts` are registers with two authors — ksor owns the entries it ships
13081
+ * (they are what the emitted site is built and tested against, and a security
13082
+ * bump reaches an existing project through exactly this path), and the adopter
13083
+ * owns everything else in them. A key ksor no longer ships is left alone rather
13084
+ * than deleted: this cannot tell one it retired from one the adopter added.
13085
+ *
13086
+ * Unparseable on either side falls back to the template, which is the same
13087
+ * whole-file offer every other site file gets — a diff, gated on `--write`.
13088
+ */
13089
+ function mergeSiteManifest(before, template) {
13090
+ const parse = (text) => {
13091
+ try {
13092
+ const value = JSON.parse(text);
13093
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
13094
+ } catch {
13095
+ return null;
13096
+ }
13097
+ };
13098
+ const mine = parse(before);
13099
+ const theirs = parse(template);
13100
+ if (mine === null || theirs === null) return template;
13101
+ const section = (key) => {
13102
+ const a = mine[key];
13103
+ const b = theirs[key];
13104
+ const table = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
13105
+ const ours = table(b);
13106
+ const adopters = table(a);
13107
+ if (ours === null) return adopters ?? void 0;
13108
+ if (adopters === null) return ours;
13109
+ const merged = {};
13110
+ for (const [name, version] of Object.entries(adopters)) merged[name] = name in ours ? ours[name] : version;
13111
+ for (const [name, version] of Object.entries(ours)) if (!(name in merged)) merged[name] = version;
13112
+ return merged;
13113
+ };
13114
+ const MERGED = [
13115
+ "dependencies",
13116
+ "devDependencies",
13117
+ "scripts"
13118
+ ];
13119
+ const out = {
13120
+ ...mine,
13121
+ ...theirs
13122
+ };
13123
+ for (const key of MERGED) {
13124
+ const value = section(key);
13125
+ if (value === void 0) delete out[key];
13126
+ else out[key] = value;
13127
+ }
13128
+ const eol = before.includes("\r\n") ? "\r\n" : "\n";
13129
+ return `${JSON.stringify(out, null, indentOf(before)).replaceAll("\n", eol)}${/\r?\n$/.test(before) ? eol : ""}`;
13130
+ }
13131
+ /**
13075
13132
  * Which manager this repository was scaffolded for, from what it committed.
13076
13133
  * `ksor init` reads `npm_config_user_agent` — the run that scaffolds is the
13077
13134
  * run that knows — but a migration is a different run, so it reads the tree.
@@ -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-8DkVCYYi.mjs
9
+ //#region ../content-gateway/dist/gateway-api-D8HlLys2.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
  /**
@@ -8769,11 +8769,12 @@ function validate$1(ring, token, scope, nowMs = Date.now()) {
8769
8769
  reason: null
8770
8770
  };
8771
8771
  }
8772
- const EMBED_TIMEOUT_S = envFloat("KSOR_EMBED_TIMEOUT_S", 60, 1);
8772
+ /** The per-request HTTP timeout for a document (ingest/batch) embed. */
8773
+ const EMBED_TIMEOUT_S = () => envFloat("KSOR_EMBED_TIMEOUT_S", 60, 1);
8773
8774
  /** Oracle env var: SOR_QUERY_EMBED_TIMEOUT_S. Note: query-embed.ts reads the
8774
8775
  * SAME variable with a different default (5.0) as its hard wall clock — two
8775
8776
  * deliberate reads, carried from the oracle (embedding.py:62 vs query_embed.py:44). */
8776
- const QUERY_EMBED_TIMEOUT_S = envFloat("KSOR_QUERY_EMBED_TIMEOUT_S", 10, 1);
8777
+ const QUERY_EMBED_TIMEOUT_S = () => envFloat("KSOR_QUERY_EMBED_TIMEOUT_S", 10, 1);
8777
8778
  function l2Normalize(v) {
8778
8779
  const norm = Math.sqrt(v.reduce((acc, x) => acc + x * x, 0));
8779
8780
  return norm === 0 ? v : v.map((x) => x / norm);
@@ -9120,8 +9121,8 @@ function buildShippedProvider(name, opts) {
9120
9121
  dim: opts.dim ?? 1536,
9121
9122
  documentTaskLabel: EMBED_TASK_DOCUMENT,
9122
9123
  queryTaskLabel: EMBED_TASK_QUERY,
9123
- documentTimeoutS: EMBED_TIMEOUT_S,
9124
- queryTimeoutS: QUERY_EMBED_TIMEOUT_S
9124
+ documentTimeoutS: EMBED_TIMEOUT_S(),
9125
+ queryTimeoutS: QUERY_EMBED_TIMEOUT_S()
9125
9126
  });
9126
9127
  }
9127
9128
  /** Written to yield ZERO ROWS, never an error, when the schema is absent. */
@@ -9991,9 +9992,46 @@ async function outline(client, scope, options = {}) {
9991
9992
  generation: pinned ?? rows[0]?.generation ?? null
9992
9993
  };
9993
9994
  }
9994
- let cacheMax = envInt("KSOR_EMBED_CACHE_MAX", 1e4, 1);
9995
- /** Oracle env var: SOR_QUERY_EMBED_TIMEOUT_S. */
9996
- const EMBED_WALL_TIMEOUT_S = envFloat("KSOR_QUERY_EMBED_TIMEOUT_S", 5, .1);
9995
+ /**
9996
+ * Query-embedding cache, converted from the oracle (sor-agentfactory @
9997
+ * b554f91, sor_content/lib/query_embed.py): L1 in-process LRU + SINGLE-FLIGHT
9998
+ * (concurrent identical misses share ONE paid embed), keyed with
9999
+ * model + task + dim so a model or dimension bump can never serve a stale
10000
+ * vector. Whitespace collapses; case does NOT fold (folding would change the
10001
+ * embedded text).
10002
+ *
10003
+ * A tiny CIRCUIT BREAKER guards the provider: after an embed failure, further
10004
+ * misses raise immediately for a short cooldown (cache hits still serve) —
10005
+ * during an outage every request degrades to keyword-only instantly instead
10006
+ * of each unique query paying its own failed attempt against a provider that
10007
+ * is already down. The breaker is keyed per SPACE (modelId, dim), like the
10008
+ * keys: a failing provider A must not degrade a healthy provider B.
10009
+ *
10010
+ * Conversion notes (decision 6):
10011
+ * - The oracle's optional Redis L2 (fail-open both directions, TTL
10012
+ * SOR_EMBED_CACHE_TTL, the `sor:emb:*` key scheme) is DROPPED — it was
10013
+ * multi-instance infrastructure; the L1 + single-flight carry a
10014
+ * single-process deployment. It returns, if ever, with real multi-instance
10015
+ * serving — nothing here forecloses it.
10016
+ * - The oracle's waiter-shield (`asyncio.shield`) and owner-cancel handling
10017
+ * protected the shared future from one caller's cancellation. JS promises
10018
+ * are not cancellable, so sharing the promise IS the whole mechanism: no
10019
+ * caller can cancel another, and there is no owner-cancelled path to map.
10020
+ * - `asyncio.wait_for` CANCELLED the embed on timeout; a JS promise cannot be
10021
+ * cancelled, so on timeout the losing call is abandoned (its settlement is
10022
+ * still observed, so it can never surface as an unhandled rejection) and
10023
+ * runs out its own HTTP timeout in the background.
10024
+ * - The breaker clock is Date.now() (ms) rather than a monotonic clock — the
10025
+ * 10 s cooldown is coarse, and fake-timer tests need the system clock.
10026
+ */
10027
+ /** Oracle env var: SOR_EMBED_CACHE_MAX. */
10028
+ let memoizedCacheMax;
10029
+ function currentCacheMax() {
10030
+ return memoizedCacheMax ??= envInt("KSOR_EMBED_CACHE_MAX", 1e4, 1);
10031
+ }
10032
+ /** Oracle env var: SOR_QUERY_EMBED_TIMEOUT_S. Read at use, not at module load
10033
+ * — see the cache-max note above. */
10034
+ const EMBED_WALL_TIMEOUT_S = () => envFloat("KSOR_QUERY_EMBED_TIMEOUT_S", 5, .1);
9997
10035
  const cache = /* @__PURE__ */ new Map();
9998
10036
  const inflight = /* @__PURE__ */ new Map();
9999
10037
  const breakerOpenUntilByMs = /* @__PURE__ */ new Map();
@@ -10037,10 +10075,11 @@ function breakerOpenUntil(provider) {
10037
10075
  return breakerOpenUntilByMs.get(spaceKey(provider)) ?? 0;
10038
10076
  }
10039
10077
  function withWallClock(work) {
10078
+ const wallTimeoutS = EMBED_WALL_TIMEOUT_S();
10040
10079
  return new Promise((resolve, reject) => {
10041
10080
  const timer = setTimeout(() => {
10042
- reject(new QueryEmbedTimeoutError(`query embed exceeded the ${EMBED_WALL_TIMEOUT_S}s wall clock — treated as a provider failure (degrade to keyword-only)`));
10043
- }, EMBED_WALL_TIMEOUT_S * 1e3);
10081
+ reject(new QueryEmbedTimeoutError(`query embed exceeded the ${wallTimeoutS}s wall clock — treated as a provider failure (degrade to keyword-only)`));
10082
+ }, wallTimeoutS * 1e3);
10044
10083
  work.then((value) => {
10045
10084
  clearTimeout(timer);
10046
10085
  resolve(value);
@@ -10061,7 +10100,7 @@ async function embedMiss(normalized, key, provider) {
10061
10100
  const literal = vlit(vec);
10062
10101
  cache.delete(key);
10063
10102
  cache.set(key, literal);
10064
- while (cache.size > cacheMax) {
10103
+ while (cache.size > currentCacheMax()) {
10065
10104
  const oldest = cache.keys().next().value;
10066
10105
  if (oldest === void 0) break;
10067
10106
  cache.delete(oldest);
package/dist/gateway.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { D as outlineHandler, I as searchHandler, N as readHandler, P as recordIsUndescribed, W as z$1, 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-8DkVCYYi-Bo0_Y1Y3.mjs";
1
+ import { D as outlineHandler, I as searchHandler, N as readHandler, P as recordIsUndescribed, W as z$1, 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-D8HlLys2-Ca8OnwLn.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/index.md CHANGED
@@ -51,6 +51,11 @@ instead of their training memory. The corpus grows with each implemented verb.
51
51
  current. Serving does not publish, so a first deploy with no ingest serves
52
52
  an empty record; this is the page that explains why, where ingest belongs
53
53
  (never inside the container), and how the abstention gate gets turned on.
54
+ - **[upgrading.md](./upgrading.md)** — moving an existing record onto a newer
55
+ ksor. `ksor migrate` offers a diff and changes nothing without `--write`;
56
+ `--write-site` is the one flag to remember, because it is the only path by
57
+ which a dependency bump reaches a project already scaffolded. Includes the
58
+ list of files migrate does NOT carry, so you know what to diff by hand.
54
59
  - **[authorization.md](./authorization.md)** — putting the record behind an
55
60
  authorization server, with worked recipes for two of them, executed rather
56
61
  than written. `ksor serve` refuses to boot unauthenticated on a public bind,
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: Upgrading a record to a newer ksor
3
+ status: draft
4
+ ---
5
+
6
+ # Upgrading a record to a newer ksor
7
+
8
+ Your repository is yours. `ksor init` copied files into it and stopped owning
9
+ them the moment it did (decision 4), so upgrading is never something a release
10
+ does to you — it is `ksor migrate` **offering** a diff you read and apply.
11
+
12
+ Nothing here changes a byte until you pass `--write`.
13
+
14
+ ## The four steps
15
+
16
+ ```sh
17
+ # 1. take the new tool
18
+ pnpm add -w @panaversity/ksor@latest # npm i / bun add — whichever scaffolded this
19
+
20
+ # 2. read what it would change
21
+ pnpm exec ksor migrate --instance instance.md --write-site
22
+
23
+ # 3. apply it
24
+ pnpm exec ksor migrate --instance instance.md --write-site --write --actor human:<your-id>
25
+
26
+ # 4. rebuild, and check
27
+ pnpm build && pnpm check
28
+ ```
29
+
30
+ `--actor` is required for step 3 whenever the migration touches governance, and
31
+ the tool will not guess one: a ledger entry that names a person who was never
32
+ there is worse than no entry (decision 21). Use the identifier your
33
+ `.ksor/governance.yaml` already knows you by.
34
+
35
+ Run step 2 on a clean working tree. The diff is the review, and it is much
36
+ easier to read when nothing else is uncommitted.
37
+
38
+ ## What `migrate` carries
39
+
40
+ | | |
41
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------------- |
42
+ | `instance.md` | the format bump and the keys that moved |
43
+ | `knowledge/**` | frontmatter into the current profile — statuses, audiences, the trust block, instants |
44
+ | `.ksor/takedowns.yaml`, `.ksor/governance.yaml` | the ledger and the policy, including denials that lived only in a database |
45
+ | `.gitignore` | the entries a new release needs negated |
46
+ | `.agents/` and `.claude/` format-checker | the emitted checker, so your own `pnpm check` and your CI agree with the tool |
47
+ | root `package.json` **scripts** | scripts a release broke — a removed flag, a step that now needs `ksor build` in front of it |
48
+ | `system/site/**` | **only with `--write-site`** — every file of the site this release emits |
49
+
50
+ `--write-site` is the one to remember, because it is the only path by which a
51
+ security bump reaches an existing project: the site's `package.json` is where
52
+ `next`, `react` and the Fumadocs pins live, and nothing else updates them.
53
+
54
+ It is an **update, never a creation**. A record with no `system/site` of its own
55
+ is not given one.
56
+
57
+ ### The site manifest is merged, not replaced
58
+
59
+ Every other file under `system/site` is reissued whole. `system/site/package.json`
60
+ is not, because it is a register with two authors: ksor owns the entries it
61
+ ships, you own everything else. So a dependency or script you added survives the
62
+ upgrade, and the pins ksor ships move to the new versions. An entry ksor no
63
+ longer ships is left alone rather than deleted — the tool cannot tell one it
64
+ retired from one you added.
65
+
66
+ ## What `migrate` does not carry
67
+
68
+ These are yours, and no release touches them. Diff them against a fresh
69
+ `ksor init` in a scratch directory when a release note says they changed:
70
+
71
+ - `Dockerfile` and `.dockerignore`
72
+ - `vercel.json` (or whatever your host reads)
73
+ - `.github/workflows/validate.yml`
74
+ - `.env.example`
75
+ - `README.md`, `AGENTS.md`, `CLAUDE.md` at the repository root
76
+ - `pnpm-workspace.yaml` / `.npmrc` and any lockfile
77
+
78
+ ```sh
79
+ npx @panaversity/ksor@latest init /tmp/fresh
80
+ diff -ru /tmp/fresh/vercel.json ./vercel.json
81
+ ```
82
+
83
+ ## What refuses, and why that is the point
84
+
85
+ `migrate` stops rather than inventing. `ksor-migrate-underivable` names the one
86
+ thing it cannot know — a title, a description, a `generated.at`, or the actor
87
+ behind a takedown it found in a database — and tells you the flag or the edit
88
+ that supplies it. Two you will meet often:
89
+
90
+ - **A record that declares `database:`** refuses until `KSOR_DB_URL` is
91
+ exported, because denials living only in that database would be republished by
92
+ a migration that never read them. Export it, or remove `database:` if the
93
+ record no longer has one.
94
+ - **`approved` documents become `draft`** unless `--approve-by <actor>` says who
95
+ approves them in the same act. Approval is a human act and a migration is not
96
+ a human.
97
+
98
+ ## After it applies
99
+
100
+ `pnpm build` regenerates every index and rewrites `build.lock.json`; `pnpm check`
101
+ runs the record checker that shipped with the new tool. If you serve the record,
102
+ `ksor schema --apply` and then a full `pnpm refresh` publish a generation the new
103
+ door can read — and a calibrated `vector_floor` measured under an older serving
104
+ predicate must be re-measured with `ksor calibrate`, because a floor measured
105
+ against a different predicate is a declared-but-uncalibrated floor and the door
106
+ will refuse every search until it is replaced.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.49",
3
+ "version": "0.0.51",
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",