@panaversity/ksor 0.0.6 → 0.0.7

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,17 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - fcd44db: feat: restarting an unedited record is free. `ksor serve` runs ingest on every
8
+ start, and ingest now compares the corpus it just read against the generation
9
+ already serving — identical content at the same source commit consumes no
10
+ generation, writes no rows, and embeds nothing ("unchanged — generation N
11
+ already serves this corpus"). Editing a document still builds a generation and
12
+ re-embeds only what changed, and a new source commit over identical bytes still
13
+ records one, because that is a build fact provenance must keep.
14
+
3
15
  ## 0.0.6
4
16
 
5
17
  ### Patch Changes
package/dist/cli.mjs CHANGED
@@ -16,7 +16,7 @@ import { bodyLimit } from "hono/body-limit";
16
16
  import { parseArgs } from "node:util";
17
17
  import { readFile, readdir, stat } from "node:fs/promises";
18
18
  import { spawnSync } from "node:child_process";
19
- //#region ../content-gateway/dist/main-MwJTMEjf.mjs
19
+ //#region ../content-gateway/dist/main-C5YO0opc.mjs
20
20
  /**
21
21
  * The pool checkout timed out — never retried: under saturation a retry is
22
22
  * a thundering herd aimed at the component already drowning.
@@ -3050,7 +3050,7 @@ async function runScopedIn(pool, gucs, op, options = {}) {
3050
3050
  throw lastError;
3051
3051
  }
3052
3052
  //#endregion
3053
- //#region ../content/dist/commands-D8yo1t8o.mjs
3053
+ //#region ../content/dist/commands-I0bYSDS5.mjs
3054
3054
  /**
3055
3055
  * EVAL-LOCKED constants, quarried verbatim from the oracle
3056
3056
  * (sor-agentfactory @ b554f91, config.py) — changing any of these is a
@@ -5864,6 +5864,53 @@ async function finalize(client, opts) {
5864
5864
  * finalize (+ optional flip, same transaction). Returns the report; the CLI
5865
5865
  * turns `refusal` into stderr + exit 1.
5866
5866
  */
5867
+ async function activeGenerationOf(c, tenantId, corpusId) {
5868
+ const raw = (await c.query("SELECT active_generation FROM corpora WHERE tenant_id = $1 AND corpus_id = $2", [tenantId, corpusId])).rows[0]?.active_generation ?? null;
5869
+ return raw === null ? null : Number(raw);
5870
+ }
5871
+ /**
5872
+ * Do two generations hold the same corpus? Compared on the SET of
5873
+ * (stable_id, content_hash) pairs — identity plus content — so a moved
5874
+ * document, an edited body, an added or removed file all count as different,
5875
+ * while a rebuild of identical bytes does not.
5876
+ */
5877
+ async function sameCorpus(c, tenantId, a, b) {
5878
+ return (await c.query(`WITH pair AS (
5879
+ SELECT s.generation, n.stable_id, s.content_hash
5880
+ FROM sources s JOIN content_nodes n
5881
+ ON n.tenant_id = s.tenant_id AND n.generation = s.generation AND n.node_id = s.node_id
5882
+ WHERE s.tenant_id = $1 AND s.generation IN ($2, $3)
5883
+ )
5884
+ SELECT (SELECT count(*) FROM pair WHERE generation = $2) =
5885
+ (SELECT count(*) FROM pair WHERE generation = $3)
5886
+ AND NOT EXISTS (
5887
+ SELECT 1 FROM pair x WHERE x.generation = $2
5888
+ AND NOT EXISTS (SELECT 1 FROM pair y WHERE y.generation = $3
5889
+ AND y.stable_id = x.stable_id AND y.content_hash = x.content_hash)
5890
+ ) AS same`, [
5891
+ tenantId,
5892
+ a,
5893
+ b
5894
+ ])).rows[0]?.same === true;
5895
+ }
5896
+ /** Was the active generation produced by this same source commit? */
5897
+ async function sameCommit(c, tenantId, generation, sourceCommit) {
5898
+ const r = await c.query(`SELECT source_commit FROM ingestion_runs
5899
+ WHERE tenant_id = $1 AND generation = $2
5900
+ ORDER BY run_id DESC LIMIT 1`, [tenantId, generation]);
5901
+ if (r.rows.length === 0) return false;
5902
+ const stored = r.rows[0]?.source_commit ?? null;
5903
+ return String(stored ?? "") === String(sourceCommit ?? "");
5904
+ }
5905
+ /** Thrown inside the build transaction to roll it back when nothing changed. */
5906
+ var UnchangedCorpus = class extends Error {
5907
+ activeGeneration;
5908
+ constructor(activeGeneration) {
5909
+ super("corpus unchanged");
5910
+ this.name = "UnchangedCorpus";
5911
+ this.activeGeneration = activeGeneration;
5912
+ }
5913
+ };
5867
5914
  async function buildGeneration(pool, instance, options) {
5868
5915
  const log = options.onLog ?? (() => void 0);
5869
5916
  const provider = options.provider;
@@ -5875,27 +5922,57 @@ async function buildGeneration(pool, instance, options) {
5875
5922
  onSkip: log
5876
5923
  });
5877
5924
  const manifestSha256 = "sha256:" + createHash("sha256").update(JSON.stringify(manifestToJson(manifest)), "utf8").digest("hex");
5878
- const { runId, generation, stats } = await runIngest(pool, tenant, async (c) => {
5879
- const alloc = await allocateRun(c, {
5880
- tenantId: tenant,
5881
- corpusId: instance.corpusId,
5882
- sourceCommit: options.sourceCommit,
5883
- manifestSha256
5884
- });
5885
- const stats = await buildStructure(c, {
5886
- tenantId: tenant,
5887
- corpusId: instance.corpusId,
5888
- generation: alloc.generation,
5889
- manifest,
5890
- files: sources,
5891
- treeRoot: options.knowledgeDir,
5892
- modelId
5925
+ let structure;
5926
+ try {
5927
+ structure = await runIngest(pool, tenant, async (c) => {
5928
+ const alloc = await allocateRun(c, {
5929
+ tenantId: tenant,
5930
+ corpusId: instance.corpusId,
5931
+ sourceCommit: options.sourceCommit,
5932
+ manifestSha256
5933
+ });
5934
+ const stats = await buildStructure(c, {
5935
+ tenantId: tenant,
5936
+ corpusId: instance.corpusId,
5937
+ generation: alloc.generation,
5938
+ manifest,
5939
+ files: sources,
5940
+ treeRoot: options.knowledgeDir,
5941
+ modelId
5942
+ });
5943
+ const active = await activeGenerationOf(c, tenant, instance.corpusId);
5944
+ if (active !== null && await sameCommit(c, tenant, active, options.sourceCommit) && await sameCorpus(c, tenant, active, alloc.generation)) throw new UnchangedCorpus(active);
5945
+ return {
5946
+ ...alloc,
5947
+ stats
5948
+ };
5893
5949
  });
5894
- return {
5895
- ...alloc,
5896
- stats
5897
- };
5898
- });
5950
+ } catch (error) {
5951
+ if (error instanceof UnchangedCorpus) {
5952
+ log(`unchanged: generation ${error.activeGeneration} already serves this corpus`);
5953
+ return {
5954
+ runId: 0,
5955
+ generation: error.activeGeneration,
5956
+ nodes: 0,
5957
+ sources: 0,
5958
+ chunks: 0,
5959
+ carried: 0,
5960
+ embedded: 0,
5961
+ failed: 0,
5962
+ ready: true,
5963
+ centroids: 0,
5964
+ flipped: false,
5965
+ refusal: null,
5966
+ health: {
5967
+ ok: true,
5968
+ reasons: []
5969
+ },
5970
+ unchanged: true
5971
+ };
5972
+ }
5973
+ throw error;
5974
+ }
5975
+ const { runId, generation, stats } = structure;
5899
5976
  log(`run ${runId}: building generation ${generation} (embed ${provider.providerId}:${provider.recipe})`);
5900
5977
  log(`structure: ${stats.nodes} nodes, ${stats.sources} sources, ${stats.chunks} chunks; carried ${stats.carried}, pending ${stats.pending}`);
5901
5978
  const pending = rowsToInputs(await runIngest(pool, tenant, async (c) => {
@@ -5997,7 +6074,8 @@ async function buildGeneration(pool, instance, options) {
5997
6074
  centroids: fin.centroids,
5998
6075
  flipped: fin.flipped,
5999
6076
  refusal: fin.refusal,
6000
- health: fin.health
6077
+ health: fin.health,
6078
+ unchanged: false
6001
6079
  };
6002
6080
  }
6003
6081
  /** Python f"{x:.0%}" analogue. */
@@ -6205,6 +6283,10 @@ async function ingestCommand(args) {
6205
6283
  throw exc;
6206
6284
  }
6207
6285
  });
6286
+ if (report.unchanged) {
6287
+ process.stdout.write(`ingest: unchanged — generation ${report.generation} already serves this corpus\n`);
6288
+ return 0;
6289
+ }
6208
6290
  process.stdout.write(`ingest: generation ${report.generation} — ${report.nodes} nodes, ${report.chunks} chunks; embedded ${report.embedded}, carried ${report.carried}, failed ${report.failed}\n`);
6209
6291
  if (report.refusal !== null) return fail$1(REFUSED, report.refusal);
6210
6292
  if (!report.flipped) process.stdout.write("ready; flip withheld (pass --flip to activate)\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Knowledge System of Record — the authoritative, governed source of knowledge that humans and AI agents operate from. Name reserved; implementation in progress.",
5
5
  "keywords": [
6
6
  "abstention",
@@ -108,8 +108,14 @@ Stand it up in this order (each step's errors explain how to fix themselves):
108
108
  unchanged chunks carry forward by content hash, so a rerun on an untouched
109
109
  corpus makes **zero provider calls** (`embedded 0, carried N`).
110
110
 
111
- What a rerun does spend is a generation each one is created and activated,
112
- and they accumulate. Reap them when you think of it, or on a schedule:
111
+ A rerun on an unchanged record costs **nothing at all**: ingest compares the
112
+ corpus it just read against the generation already serving and, when they
113
+ are identical at the same commit, consumes no generation and writes no rows
114
+ ("unchanged — generation N already serves this corpus"). Edit a document and
115
+ the next run builds a generation for it, re-embedding only what changed.
116
+
117
+ Generations do accumulate as you edit. Reap the superseded ones when you
118
+ think of it, or on a schedule:
113
119
 
114
120
  ```sh
115
121
  pnpm exec ksor gc --instance instance.md
@@ -45,8 +45,9 @@ the NAME of the variable, never the DSN. That is the whole required config:
45
45
  (turn it on afterwards with `ksor calibrate`, once the record is serving).
46
46
 
47
47
  `pnpm serve` is the only command this rung needs — first run, after editing
48
- `knowledge/`, or just to bring the server back. It re-embeds only what changed,
49
- so a rerun on an untouched corpus costs no provider calls. `AGENTS.md` "Serving to agents" is the
48
+ `knowledge/`, or just to bring the server back. A rerun on an unchanged record
49
+ costs nothing: no new generation, no embedding, no rows. Edit a document and
50
+ the next run picks up exactly that change. `AGENTS.md` → "Serving to agents" is the
50
51
  full runbook; your coding agent reads it first. `pnpm serve` binds loopback
51
52
  with auth off for local use; a public bind fails closed unless auth is
52
53
  configured. Any other operation is `pnpm exec ksor <verb>`.