@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 +12 -0
- package/dist/cli.mjs +105 -23
- package/package.json +1 -1
- package/templates/scaffold/AGENTS.md +8 -2
- package/templates/scaffold/README.md +3 -2
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-
|
|
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-
|
|
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
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
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
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
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.
|
|
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
|
-
|
|
112
|
-
|
|
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.
|
|
49
|
-
|
|
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>`.
|