@panaversity/ksor 0.0.5 → 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,33 @@
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
+
15
+ ## 0.0.6
16
+
17
+ ### Patch Changes
18
+
19
+ - 3890ad2: fix: a scaffolded project has exactly two commands, one per surface —
20
+ `pnpm dev` for the site people read, `pnpm serve` for the record agents query
21
+ (it applies the schema, authorizes ingest, ingests, and serves). Neither asks
22
+ the reader to decide anything.
23
+
24
+ fix: the one-command script is no longer called `up`. `up` is
25
+ pnpm's own alias for `update`, so the script shipped in 0.0.5 was shadowed by
26
+ the package manager: an adopter following the runbook ran `pnpm up` expecting
27
+ to bring their record up and instead upgraded their dependencies. Anyone on
28
+ 0.0.5 should use `pnpm run schema && pnpm run grant && pnpm run ingest &&
29
+ pnpm run serve` until they re-scaffold.
30
+
3
31
  ## 0.0.5
4
32
 
5
33
  ### 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.5",
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",
@@ -93,28 +93,39 @@ Stand it up in this order (each step's errors explain how to fix themselves):
93
93
  3. **Bring it up — one command:**
94
94
 
95
95
  ```sh
96
- pnpm up # schema → grant → ingest → serve
96
+ pnpm serve # schema → grant → ingest → serve
97
97
  ```
98
98
 
99
99
  Every step is re-runnable, so this is also how you **refresh after editing
100
100
  `knowledge/`**: an applied schema reports "already applied", an existing
101
101
  grant reports "already granted", and ingest builds a fresh generation.
102
102
 
103
- **`pnpm up` ingests every time but it re-EMBEDS nothing that has not
104
- changed.** Chunks carry forward by content hash, so a rerun on an untouched
105
- corpus makes zero provider calls (`embedded 0, carried N` in the output).
106
- What it does spend is a generation: each run creates and activates a new one,
107
- and they accumulate. So:
108
-
109
- | You want to | Run |
110
- | -------------------------------- | ------------------------------------------ |
111
- | set up, or refresh after an edit | `pnpm up` |
112
- | just restart the server | `pnpm serve` no new generation |
113
- | reap superseded generations | `pnpm exec ksor gc --instance instance.md` |
114
-
115
- Run the steps individually (`pnpm schema`, `pnpm grant`, `pnpm ingest`)
116
- when the acts belong to different people — a DBA holding the credentials
117
- that authorize ingest, for instance.
103
+ **`pnpm serve` is the only command this rung needs.** Run it the first
104
+ time, run it after editing `knowledge/`, run it to bring the server back —
105
+ it is always the right answer, so there is nothing to decide. Every step it
106
+ chains reports the state it found rather than failing: an applied schema
107
+ says "already applied", an existing grant says "already granted", and
108
+ unchanged chunks carry forward by content hash, so a rerun on an untouched
109
+ corpus makes **zero provider calls** (`embedded 0, carried N`).
110
+
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:
119
+
120
+ ```sh
121
+ pnpm exec ksor gc --instance instance.md
122
+ ```
123
+
124
+ The individual verbs (`pnpm schema`, `pnpm grant`, `pnpm ingest`,
125
+ `pnpm serve`) exist for pipelines and split duties — a deploy step that
126
+ ingests while a different process serves, or a DBA who holds the credentials
127
+ that authorize ingest. Reach for them when something else runs the steps;
128
+ not as a daily choice.
118
129
 
119
130
  4. **Turn the abstention gate on — deliberately, once it serves.** This is the
120
131
  step that makes "not in this corpus" a real answer, and it is measured, never
@@ -31,7 +31,7 @@ part of `pnpm dev`. The ordered path is:
31
31
 
32
32
  ```sh
33
33
  cp .env.example .env # fill in KSOR_DB_URL, GEMINI_API_KEY, KSOR_AUTH_DISABLED=1
34
- pnpm up # schema → grant → ingest → serve
34
+ pnpm serve # schema → grant → ingest → serve
35
35
  ```
36
36
 
37
37
  `ksor` reads `.env` automatically — nothing to export. `KSOR_AUTH_DISABLED=1`
@@ -44,10 +44,10 @@ the NAME of the variable, never the DSN. That is the whole required config:
44
44
  `retrieval:` out starts you with the abstention gate off and honest about it
45
45
  (turn it on afterwards with `ksor calibrate`, once the record is serving).
46
46
 
47
- `pnpm up` is re-runnable it is also how you refresh after editing
48
- `knowledge/`. It re-ingests each time but re-embeds only what changed, so an
49
- untouched corpus costs no provider calls; to simply restart the server without
50
- building a new generation, run `pnpm serve` on its own. `AGENTS.md` → "Serving to agents" is the
47
+ `pnpm serve` is the only command this rung needs first run, after editing
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
51
51
  full runbook; your coding agent reads it first. `pnpm serve` binds loopback
52
52
  with auth off for local use; a public bind fails closed unless auth is
53
53
  configured. Any other operation is `pnpm exec ksor <verb>`.
@@ -76,7 +76,7 @@ different coding agent's way of finding the same working contract.
76
76
  | `.gitattributes` | markdown is checked out byte-stable on every platform, so the same commit hashes the same everywhere. |
77
77
  | `.env.example` | the variables the served rung needs; copy to `.env` (gitignored) and fill in. |
78
78
  | `.gitignore` | keeps build output, `node_modules/`, and `.env` out of the record's history. |
79
- | `package.json` | the `pnpm dev` / `pnpm build` / `pnpm check` commands and the served rung's `pnpm up` (schema → grant → ingest → serve, or run them separately), the pinned `@panaversity/ksor` tool, and the pnpm version this project pins. |
79
+ | `package.json` | the two surface commands `pnpm dev` (the site) and `pnpm serve` (the agent surface: schema → grant → ingest → serve) plus `pnpm build` / `pnpm check`, the pinned `@panaversity/ksor` tool, and the pnpm version this project pins. |
80
80
  | `pnpm-workspace.yaml` | where the workspace looks for code (`system/site`, plus reserved `system/gateways/*` and `system/packages/*`), and the supply-chain policy for installs. |
81
81
  | `pnpm-lock.yaml` | the exact dependency versions — the reason two machines build the same site. |
82
82
 
@@ -7,11 +7,10 @@
7
7
  "dev": "pnpm -C system/site dev",
8
8
  "build": "pnpm -C system/site build",
9
9
  "check": "node .agents/skills/format-checker/check.mjs",
10
- "up": "pnpm schema && pnpm grant && pnpm ingest && pnpm serve",
10
+ "serve": "pnpm schema && pnpm grant && pnpm ingest && ksor serve",
11
11
  "schema": "ksor schema --instance instance.md --apply",
12
12
  "grant": "ksor grant --instance instance.md",
13
- "ingest": "ksor ingest --instance instance.md --knowledge knowledge --flip",
14
- "serve": "ksor serve"
13
+ "ingest": "ksor ingest --instance instance.md --knowledge knowledge --flip"
15
14
  },
16
15
  "dependencies": {
17
16
  "@panaversity/ksor": "KSOR-STAMP-VERSION"