@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 +28 -0
- package/dist/cli.mjs +105 -23
- package/package.json +1 -1
- package/templates/scaffold/AGENTS.md +27 -16
- package/templates/scaffold/README.md +6 -6
- package/templates/scaffold/package.json +2 -3
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-
|
|
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",
|
|
@@ -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
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
48
|
-
`knowledge
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
"
|
|
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"
|