@panaversity/ksor 0.0.18 → 0.0.19

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,59 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.19
4
+
5
+ ### Patch Changes
6
+
7
+ - aa4bdce: Record why the vector index is unused, and what fixing it would cost
8
+
9
+ Diagnosis only — no serving behaviour changes. Answers are unaffected, and were
10
+ already correct: the query plans a sequential scan, and a sequential scan is
11
+ EXACT. What grows with the corpus is the work, not the error.
12
+
13
+ The cause recorded until now — a window function, then joins and predicates
14
+ Postgres cannot estimate — was incomplete. Testing each clause on its own shows
15
+ a cost mispricing underneath: a full sequential pass over 20,000 chunks,
16
+ including 20,000 1536-dimension distance computations, is priced at 1904 for
17
+ work that takes ~130 ms, while the HNSW scan's startup cost alone is 2137.
18
+
19
+ A restructured arm reaches 36 ms against 648 ms — but only with `ef_search` at
20
+ pgvector's default, which is the setting where the index missed the true nearest
21
+ neighbour for 1 query in 100 on a bed with real cluster structure, dropping the
22
+ top-1 similarity by 0.99. Against this record's ~0.01 abstention separation,
23
+ that flips an abstention: the corpus holds the answer and the door says it does
24
+ not. The speed and the approximation cannot be separated, so taking them is an
25
+ owner decision rather than a tuning change.
26
+
27
+ Both the current plan and the fix path are now pinned by tests, so neither can
28
+ drift unnoticed.
29
+
30
+ - b9f3d00: A citation pin no longer outlives a restriction
31
+
32
+ A snapshot token pins a generation so a citation keeps resolving to the same
33
+ bytes. It was also deciding the _audience_ question — evaluating `visibility` on
34
+ the pinned row — so a document restricted after the token was issued kept reading
35
+ in full for the token's life, to a caller the record had just closed it to.
36
+
37
+ Three routes refused it and one served it, on the same surface, in the same
38
+ second: `outline` omitted it, `search` filtered it, an unpinned `read` refused it,
39
+ and `read` with a pre-flip token returned the whole document.
40
+
41
+ The generation pointers are why the obvious guard missed it. A flip sets
42
+ `rollback_generation` to the generation just superseded, so a pre-flip pin is
43
+ exactly the rollback pointer — servable by design, and the check that narrows a
44
+ pin to {active, rollback} passed it.
45
+
46
+ **Governance is now read from the record as it stands.** A pin still decides
47
+ which generation's content is served; it no longer decides whether the caller may
48
+ have it. A document the record no longer contains cannot be resurrected by one
49
+ either. Unpinned reads are unaffected — with nothing pinned, the two generations
50
+ are the same one and the check is an identity.
51
+
52
+ The cost is deliberate: a citation can stop resolving within the token's 30
53
+ minutes when the record restricts what it points at. That is what "the record
54
+ changed" should look like. The alternative is a window in which a withdrawal is
55
+ not a withdrawal.
56
+
3
57
  ## 0.0.18
4
58
 
5
59
  ### Patch Changes
package/dist/cli.mjs CHANGED
@@ -15,7 +15,7 @@ import { bodyLimit } from "hono/body-limit";
15
15
  import { execFileSync, spawnSync } from "node:child_process";
16
16
  import { parseArgs } from "node:util";
17
17
  import { readFile, readdir, stat } from "node:fs/promises";
18
- //#region ../content-gateway/dist/main-Deg5kd9y.mjs
18
+ //#region ../content-gateway/dist/main-DDeyGVjK.mjs
19
19
  /**
20
20
  * A connection could not be ESTABLISHED in time — retryable.
21
21
  *
@@ -2053,6 +2053,28 @@ g AS (
2053
2053
  (SELECT active_generation FROM corpora
2054
2054
  WHERE tenant_id = $1 AND corpus_id = $2)
2055
2055
  ) AS gen
2056
+ ),
2057
+ -- The generation the record is on RIGHT NOW, which decides governance even when
2058
+ -- content is served from a pinned one.
2059
+ --
2060
+ -- A snapshot pin exists so a citation keeps resolving to the same bytes. It used
2061
+ -- to decide the audience question too, by evaluating visibility on the pinned
2062
+ -- row — so a document restricted after the token was issued kept reading in full
2063
+ -- for the token's life, while outline, search and an unpinned read all
2064
+ -- refused it in the same second. servableGenerations could not catch it: a
2065
+ -- flip sets rollback_generation to the generation just superseded, so a pre-flip
2066
+ -- pin IS the rollback pointer and is servable by design (issue #87).
2067
+ --
2068
+ -- Pins yield. A citation may stop resolving within the token's life, which is
2069
+ -- what "the record changed" should look like — the alternative is a window in
2070
+ -- which a withdrawal is not a withdrawal, and decision 19 says a surface that
2071
+ -- refuses must refuse everywhere, which includes its own fourth route.
2072
+ --
2073
+ -- When nothing is pinned this is the SAME generation as g, so the join is an
2074
+ -- identity and no unpinned read changes behaviour.
2075
+ live AS (
2076
+ SELECT active_generation AS gen FROM corpora
2077
+ WHERE tenant_id = $1 AND corpus_id = $2
2056
2078
  )`;
2057
2079
  /** Candidates by LEAF slug ($4), each with its full root path (for suffix disambiguation). */
2058
2080
  const NODE_BY_SLUG_SQL = `
@@ -2082,7 +2104,12 @@ SELECT n.node_id, n.slug, n.title, n.stable_id, n.path, n.generation, n.permalin
2082
2104
  FROM tree n
2083
2105
  JOIN content_nodes self ON self.node_id = n.node_id AND self.tenant_id = $1
2084
2106
  AND self.generation = n.generation
2085
- WHERE n.slug = $4 AND ${DENY$1} AND ${audienceAllowed$1("self")}
2107
+ -- INNER join, so a document the record no longer contains cannot be
2108
+ -- resurrected by a pin either: no live row, no read.
2109
+ JOIN live ON TRUE
2110
+ JOIN content_nodes now ON now.tenant_id = $1 AND now.generation = live.gen
2111
+ AND now.stable_id = self.stable_id
2112
+ WHERE n.slug = $4 AND ${DENY$1} AND ${audienceAllowed$1("now")}
2086
2113
  ORDER BY n.path`;
2087
2114
  const ALIAS_SQL = `
2088
2115
  WITH ${GEN}
@@ -2099,8 +2126,11 @@ const NODE_BY_STABLE_ID_SQL = `
2099
2126
  WITH RECURSIVE ${GEN}, ${DENIED_CTE$1}
2100
2127
  SELECT n.node_id, n.slug, n.title, n.stable_id, n.stable_id::text AS path, n.generation, n.permalink
2101
2128
  FROM content_nodes n JOIN g ON n.generation = g.gen
2129
+ JOIN live ON TRUE
2130
+ JOIN content_nodes now ON now.tenant_id = $1 AND now.generation = live.gen
2131
+ AND now.stable_id = n.stable_id
2102
2132
  WHERE n.tenant_id = $1 AND n.stable_id = $4 AND n.status = 'published' AND ${DENY$1}
2103
- AND ${AUDIENCE_ALLOWED$1}`;
2133
+ AND ${audienceAllowed$1("now")}`;
2104
2134
  const DOCUMENT_CHUNKS_SQL = `
2105
2135
  WITH ${GEN}
2106
2136
  SELECT c.ordinal, COALESCE(c.heading_path_text, ''), c.content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
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",