@panaversity/ksor 0.0.13 → 0.0.15

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/cli.mjs +94 -32
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,139 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 5b076e6: Short documents reach search again — navigation is a shape, not a length
8
+
9
+ A record could be fully ingested, report "embedded 16, failed 0", and still be
10
+ unable to answer questions it plainly contained. Sections were classified as
11
+ navigation by LENGTH — anything under 250 characters — and navigation is
12
+ excluded from every retrieval arm. On a handbook that inverts the intent,
13
+ because a handbook's most valuable statements are its shortest.
14
+
15
+ Walked on 0.0.14 with three ordinary policy statements — a refund window, an
16
+ escalation path, a badge rule, 200-300 characters each. Three of four chunks
17
+ were unsearchable, and:
18
+
19
+ > **Q.** "how long does a buyer have to send something back"
20
+ > **A.** the scaffold's placeholder page — against a record stating _thirty days_
21
+
22
+ The answer was in the corpus, correctly ingested, readable by slug, and
23
+ unreachable by search.
24
+
25
+ Navigation is now decided by shape: a section is navigation when link lines are
26
+ most of it, or when what remains after them is too short to answer anything —
27
+ the same floor the serving predicate already applies. Length is no longer
28
+ consulted, so a 180-character link list is navigation and a 51-character fact is
29
+ not, which is the ordering length had backwards.
30
+
31
+ Measured on an authored handbook gold set with real embeddings, paired: short
32
+ substantive facts went **0/9 to 9/9 at rank 1**, the long-prose control held at
33
+ **4/4**, and the link-list page was returned **0** times. That last number is the
34
+ one that matters — admitting everything would have improved the first two and
35
+ made the product worse.
36
+
37
+ **To pick this up, re-run `ksor ingest`.** Chunks are re-classified on every
38
+ build and unchanged content is not re-embedded, so the upgrade costs a build,
39
+ not an embedding bill. `CHUNK_POLICY` moves to v6 because it is persisted
40
+ provenance and the behaviour it labels has changed.
41
+
42
+ - 5763e8b: Internal: a pool test that raced Postgres, and a comment that had it backwards
43
+
44
+ No adopter-visible behaviour changes.
45
+
46
+ `idle.db.test.ts` sampled `pg_stat_activity` immediately after a previous test's
47
+ `pool.end()`. Those are two different clocks — `end()` resolves when the client
48
+ socket closes, while the row disappears only once the server-side backend
49
+ actually exits — so the suite was order-coupled through the database and went
50
+ red in CI on a branch that changed nothing but a document. Each test now waits
51
+ for a quiet database before it starts, and states that it does.
52
+
53
+ The comment added in the previous release explaining the `env.example` guard fix
54
+ described the rename backwards: the TEMPLATE holds `env.example` and
55
+ `materialize.ts` maps it to `.env.example` on emit, not the other way round.
56
+
57
+ ## 0.0.14
58
+
59
+ ### Patch Changes
60
+
61
+ - a0d98b0: Cut dead weight, and repair two guards that had quietly stopped guarding
62
+
63
+ A sweep across every package, with each candidate handed to a second reviewer
64
+ whose job was to prove it still alive. Net −154 lines. Nothing an adopter can
65
+ observe changes; two things that were supposed to fail no longer stay silent.
66
+
67
+ **The two repairs.** A guard asserting that no scaffolded document describes
68
+ serving as publishing — a claim this repo has had to correct four times — ran
69
+ `readFileSync` inside a `try` whose `catch` returned quietly, and one of its five
70
+ filenames was `.env.example` while the scaffold emits `env.example`. So the row
71
+ covering the file that actually carries the serving variables had never executed.
72
+ The name is fixed and a missing file now fails instead of passing. Separately,
73
+ two doc-blocks described a stdio transport in the present tense; there is no
74
+ stdio door in the product, and the suite claiming to drive one drives HTTP.
75
+
76
+ **The removals.** A 134-line live-walk script pinned to `@panaversity/ksor@0.0.4`
77
+ that nothing referenced. `AuthConfig.jwksUrl`, computed and stored but never read
78
+ — its live twin is `explicitJwksUrl`; the boot-time validation of
79
+ `KSOR_JWKS_URL` stays exactly where it was. An `allowedAudiences.length > 0 &&`
80
+ operand that no path can reach as false, and whose false side would have skipped
81
+ the audience allowlist entirely. A `PoolTimeoutError` message parameter no caller
82
+ passed, which was also the one input where two retry classifiers disagreed —
83
+ removing it closes that. Two `instanceof X || instanceof Error` disjuncts where
84
+ `X extends Error`, so the first could never decide anything. One unused icon
85
+ export in the workbench shell.
86
+
87
+ **Left alone deliberately.** `SearchScope.kinds` is genuinely dead, but removing
88
+ it renumbers positional parameters across three SQL statements, two of which
89
+ derive a shared CTE by string substitution, and the test that would catch a wrong
90
+ renumber is gated on a database. That is a change to make on its own, with the
91
+ gate watching — not alongside a release.
92
+
93
+ - ce1595b: Ingest names the real reason it could not record a commit
94
+
95
+ Every first ingest of a freshly scaffolded project printed "knowledge/ is not in
96
+ a git repository". That is false: `ksor init` runs `git init`, so the repository
97
+ exists — it simply has no commit yet, and `rev-parse HEAD` fails with "unknown
98
+ revision" rather than because nothing is there. The reader was sent to `git
99
+ init`, which they had already run, in the one message that decides whether an
100
+ answer can be traced back to a reviewed commit.
101
+
102
+ Three different states were collapsing into that one sentence, and each has a
103
+ different next command:
104
+
105
+ ```
106
+ knowledge/ is in a git repository with no commits yet …
107
+ fix: commit the record (git add knowledge && git commit) and re-run
108
+
109
+ knowledge/ is not in a git repository …
110
+ fix: git init, commit the record, and re-run
111
+
112
+ git is not installed …
113
+ fix: install git, or pass --source-commit <sha> if the record is versioned elsewhere
114
+ ```
115
+
116
+ Verified on a real scaffold: the fresh case prints the first, and committing the
117
+ record turns the next ingest's `source:` line into an actual SHA.
118
+
119
+ - 474dedc: Internal: the env-contract drift test scans only the checkout's source
120
+
121
+ No adopter-visible behaviour changes. The test that guarantees every
122
+ adopter-settable environment variable is named in the scaffold's `env.example`
123
+ walked `packages/` with a `statSync` per entry, and descended into the fake npm
124
+ install another suite roots inside `packages/ksor`. That cost two ways: the
125
+ copied template sources were scanned twice, and an entry deleted between the
126
+ `readdir` and the `statSync` crashed the whole run — which is what took CI red
127
+ on run 32526491721, on an `llms.txt` being cleaned up concurrently.
128
+
129
+ The walk now takes each entry's type from the readdir snapshot itself, so a
130
+ vanishing entry cannot crash it, and it skips transient install trees, so its
131
+ input no longer depends on whether another suite is mid-run. The `REPO_ONLY`
132
+ exemption list was deleted as dead: it named seven variables that no scanned
133
+ file can contain, because the walk excludes test files in the first place. The
134
+ honesty check that is supposed to catch stale exemptions now covers every
135
+ exemption list, which is what its name always claimed.
136
+
3
137
  ## 0.0.13
4
138
 
5
139
  ### Patch Changes
package/dist/cli.mjs CHANGED
@@ -16,7 +16,7 @@ import { bodyLimit } from "hono/body-limit";
16
16
  import { execFileSync, spawnSync } from "node:child_process";
17
17
  import { parseArgs } from "node:util";
18
18
  import { readFile, readdir, stat } from "node:fs/promises";
19
- //#region ../content-gateway/dist/main-B1G1VdMp.mjs
19
+ //#region ../content-gateway/dist/main-wfyAlzsD.mjs
20
20
  /**
21
21
  * A connection could not be ESTABLISHED in time — retryable.
22
22
  *
@@ -42,8 +42,8 @@ var ConnectTimeoutError$1 = class extends Error {
42
42
  * a thundering herd aimed at the component already drowning.
43
43
  */
44
44
  var PoolTimeoutError$1 = class extends Error {
45
- constructor(detail = "the configured checkout bound") {
46
- super(`pool checkout timed out (${detail}) — the pool is saturated; shedding this request is the recovery path, retrying it is not`);
45
+ constructor() {
46
+ super("pool checkout timed out (the configured checkout bound) — the pool is saturated; shedding this request is the recovery path, retrying it is not");
47
47
  this.name = "PoolTimeoutError";
48
48
  }
49
49
  };
@@ -430,7 +430,7 @@ const EMBED_DIM$1 = 1536;
430
430
  const EMBED_TASK_DOCUMENT$1 = "RETRIEVAL_DOCUMENT";
431
431
  const EMBED_TASK_QUERY$1 = "RETRIEVAL_QUERY";
432
432
  /** bump ⇒ provenance (v5: CommonMark fences). All char limits count CODE POINTS (Python len parity). */
433
- const CHUNK_POLICY$1 = "heading-aware-1500-content-only-v5";
433
+ const CHUNK_POLICY$1 = "heading-aware-1500-content-only-v6";
434
434
  /**
435
435
  * The kernel's view of `instance.md` (adapted from oracle SC/instance.py
436
436
  * under decision 11 — an adaptation, not a port: ksor has ONE instance file
@@ -3079,7 +3079,7 @@ function buildServer(ctx, version) {
3079
3079
  structuredContent: result
3080
3080
  };
3081
3081
  } catch (error) {
3082
- if (error instanceof EmptyQueryError || error instanceof Error) return {
3082
+ if (error instanceof Error) return {
3083
3083
  content: [{
3084
3084
  type: "text",
3085
3085
  text: `Error: ${error.message}`
@@ -3188,7 +3188,7 @@ function toolError(error) {
3188
3188
  return {
3189
3189
  content: [{
3190
3190
  type: "text",
3191
- text: `Error: ${error instanceof UnknownSlug || error instanceof Error ? error.message : String(error)}`
3191
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
3192
3192
  }],
3193
3193
  isError: true
3194
3194
  };
@@ -3366,12 +3366,10 @@ function configFromEnv(env) {
3366
3366
  const allowedAudiences = (env.KSOR_JWT_ALLOWED_AUDIENCES ?? "").split(",").map((a) => a.trim()).filter((a) => a !== "");
3367
3367
  const issuer = (env.KSOR_SSO_ISSUER ?? "").trim() || null;
3368
3368
  const explicit = (env.KSOR_JWKS_URL ?? "").trim();
3369
- const jwksUrl = explicit || `${ssoUrl}/api/auth/jwks`;
3370
- assertHttpUrl("KSOR_JWKS_URL", jwksUrl, true);
3369
+ assertHttpUrl("KSOR_JWKS_URL", explicit || `${ssoUrl}/api/auth/jwks`, true);
3371
3370
  return {
3372
3371
  ssoUrl,
3373
3372
  resourceUrl,
3374
- jwksUrl,
3375
3373
  explicitJwksUrl: explicit === "" ? null : explicit,
3376
3374
  allowedAudiences,
3377
3375
  issuer,
@@ -3492,7 +3490,7 @@ function createVerify(config, deps, jwksOf) {
3492
3490
  cause: err
3493
3491
  });
3494
3492
  }
3495
- if (config.allowedAudiences.length > 0 && !audOk(claims.aud, config.allowedAudiences)) {
3493
+ if (!audOk(claims.aud, config.allowedAudiences)) {
3496
3494
  reject(key);
3497
3495
  throw new TokenVerifyError(`token aud ${JSON.stringify(claims.aud ?? null)} not in allowlist ${JSON.stringify(config.allowedAudiences)}`, { transient: false });
3498
3496
  }
@@ -3648,8 +3646,8 @@ const UNDESCRIBED_RECORD = "instance.md is still the scaffold template — agent
3648
3646
  * Composition (oracle main.py's boot order, adapted): instance → DSN via
3649
3647
  * the declared env NAME → provider → pool → space guard → service context.
3650
3648
  * Auth is built by the door that needs it (http.ts) — BEFORE the pool
3651
- * serves anything; stdio is the local loopback-equivalent door and runs
3652
- * with auth off by construction.
3649
+ * serves anything; a loopback bind is the local-equivalent door and is the
3650
+ * only posture that may run with auth explicitly disabled.
3653
3651
  */
3654
3652
  async function compose(instancePath, version) {
3655
3653
  let instanceText;
@@ -4149,8 +4147,8 @@ var ConnectTimeoutError = class extends Error {
4149
4147
  * a thundering herd aimed at the component already drowning.
4150
4148
  */
4151
4149
  var PoolTimeoutError = class extends Error {
4152
- constructor(detail = "the configured checkout bound") {
4153
- super(`pool checkout timed out (${detail}) — the pool is saturated; shedding this request is the recovery path, retrying it is not`);
4150
+ constructor() {
4151
+ super("pool checkout timed out (the configured checkout bound) — the pool is saturated; shedding this request is the recovery path, retrying it is not");
4154
4152
  this.name = "PoolTimeoutError";
4155
4153
  }
4156
4154
  };
@@ -4454,7 +4452,7 @@ async function withPgRetry(op, options = {}) {
4454
4452
  throw lastError;
4455
4453
  }
4456
4454
  //#endregion
4457
- //#region ../content/dist/commands-LOfF_iLT.mjs
4455
+ //#region ../content/dist/commands-_8HvMWfD.mjs
4458
4456
  /**
4459
4457
  * EVAL-LOCKED constants, quarried verbatim from the oracle
4460
4458
  * (sor-agentfactory @ b554f91, config.py) — changing any of these is a
@@ -4469,7 +4467,7 @@ const EMBED_DIM = 1536;
4469
4467
  const EMBED_TASK_DOCUMENT = "RETRIEVAL_DOCUMENT";
4470
4468
  const EMBED_TASK_QUERY = "RETRIEVAL_QUERY";
4471
4469
  /** bump ⇒ provenance (v5: CommonMark fences). All char limits count CODE POINTS (Python len parity). */
4472
- const CHUNK_POLICY = "heading-aware-1500-content-only-v5";
4470
+ const CHUNK_POLICY = "heading-aware-1500-content-only-v6";
4473
4471
  const MAX_CHARS = 1500;
4474
4472
  /** < Gemini's 2048-token embed input. */
4475
4473
  const HARD_MAX_CHARS = 4e3;
@@ -7884,11 +7882,48 @@ function cleanBody(rawBody) {
7884
7882
  function teachingBody(content) {
7885
7883
  return pyStrip(pySplitLines(content, false).filter((ln) => !HEADING.test(ln)).join("\n"));
7886
7884
  }
7885
+ /**
7886
+ * A line that is navigation rather than prose: strip its list marker and what
7887
+ * remains is nothing but links.
7888
+ *
7889
+ * Deliberately narrow. A line with prose AROUND a link ("Claim within thirty
7890
+ * days; see the [expenses page](x) to file.") is prose, because the sentence is
7891
+ * the content and the link is incidental.
7892
+ */
7893
+ const NAV_LINE = /^(?:[-*+]\s+|\d+[.)]\s+)?(?:\[[^\]]*\]\([^)]*\)|<https?:\/\/[^>]*>|https?:\/\/\S+)(?:[\s,;·|>—–-]*(?:\[[^\]]*\]\([^)]*\)|<https?:\/\/[^>]*>|https?:\/\/\S+))*[\s.,;:]*$/;
7894
+ /**
7895
+ * Is this segment NAVIGATION — a thing that points at content rather than
7896
+ * being content?
7897
+ *
7898
+ * The oracle answered this with length: under NAV_MAX_CHARS (250) meant nav.
7899
+ * On the curriculum corpus it was tuned against, that proxy holds — a short
7900
+ * segment there really is a link list. On a handbook it inverts, because a
7901
+ * handbook's most valuable statements are its shortest ("Six months, with a
7902
+ * written review at three and six"), and `nav` is excluded from search. Issue
7903
+ * #55, walked live on 0.0.14: three of four chunks in an ordinary policy
7904
+ * record were unsearchable, and a question the record plainly answered was
7905
+ * served the scaffold's placeholder instead.
7906
+ *
7907
+ * So the question is asked about SHAPE, which is what "navigation" always
7908
+ * meant. A segment is nav when link lines are most of it, or when what is left
7909
+ * after them is too little to answer anything (MIN_CONTENT_CHARS — the same
7910
+ * floor the serving predicate applies, so this never labels `prose` something
7911
+ * search would refuse to return anyway).
7912
+ *
7913
+ * Length is no longer consulted. A 180-character link list is nav and a
7914
+ * 51-character fact is prose, which is the ordering length got backwards.
7915
+ */
7916
+ function isNavShaped(content) {
7917
+ const lines = pySplitLines(teachingBody(content), false).map((ln) => pyStrip(ln)).filter((ln) => ln !== "");
7918
+ if (lines.length === 0) return true;
7919
+ if (lines.filter((ln) => NAV_LINE.test(ln)).length * 2 > lines.length) return true;
7920
+ return cpLen(lines.filter((ln) => !NAV_LINE.test(ln)).join(" ")) < 24;
7921
+ }
7887
7922
  function classify(content, headingPath) {
7888
7923
  if (JSX_ASSESS.test(content)) return "assessment";
7889
7924
  const leaf = headingPath.length > 0 ? headingPath[headingPath.length - 1] : "";
7890
7925
  if (JSX_EMBED.test(content) || content.includes("docs.google.com/presentation") || leaf.includes("Teaching Aid")) return "embed";
7891
- if (cpLen(teachingBody(content)) < 250) return "nav";
7926
+ if (isNavShaped(content)) return "nav";
7892
7927
  return "prose";
7893
7928
  }
7894
7929
  /** A segment DOMINATED by a line-leading widget (with < NAV_MAX_CHARS of
@@ -8008,7 +8043,7 @@ function chunkText(text, maxChars = MAX_CHARS) {
8008
8043
  } else prefix += seg.text;
8009
8044
  continue;
8010
8045
  }
8011
- const segIsNav = cpLen(teachingBody(seg.text)) < 250;
8046
+ const segIsNav = isNavShaped(seg.text);
8012
8047
  const segMarker = segmentMarkerType(seg.text);
8013
8048
  for (const piece of subsplit(seg.text, maxChars)) {
8014
8049
  let sourceType;
@@ -8692,19 +8727,46 @@ function composeProvider(instance) {
8692
8727
  return fail$1(REFUSED, `instance embedding.provider: ${exc instanceof Error ? exc.message : String(exc)}`);
8693
8728
  }
8694
8729
  }
8695
- /**
8696
- * The commit the corpus was ingested from, resolved from git when the tree is
8697
- * in a repository.
8698
- *
8699
- * `--source-commit` has always existed and the golden path never passed it, so
8700
- * EVERY generation an adopter produced recorded the literal string
8701
- * "unspecified" — product principle 6 requires a build to record the exact
8702
- * corpus that produced it, and a placeholder records nothing (review
8703
- * 2026-08-20). Resolved here rather than in the scaffold script so it is right
8704
- * however the verb is invoked. A tree that is not a repository, or a git that
8705
- * is not installed, still records the honest sentinel rather than failing an
8706
- * ingest over provenance metadata.
8707
- */
8730
+ function provenanceGap(knowledgeDir) {
8731
+ if (knowledgeDir === void 0) return "not-asked";
8732
+ const run = (args) => {
8733
+ try {
8734
+ return {
8735
+ ok: true,
8736
+ out: execFileSync("git", [
8737
+ "-C",
8738
+ knowledgeDir,
8739
+ ...args
8740
+ ], {
8741
+ encoding: "utf8",
8742
+ stdio: [
8743
+ "ignore",
8744
+ "pipe",
8745
+ "ignore"
8746
+ ]
8747
+ }).trim()
8748
+ };
8749
+ } catch {
8750
+ return {
8751
+ ok: false,
8752
+ out: ""
8753
+ };
8754
+ }
8755
+ };
8756
+ if (!run(["--version"]).ok && !run(["rev-parse", "--git-dir"]).ok) return "no-git";
8757
+ if (!run(["rev-parse", "--git-dir"]).ok) return "no-repo";
8758
+ return "no-commit";
8759
+ }
8760
+ /** The remedy for each, because the reader's next command differs. */
8761
+ function provenanceNotice(gap) {
8762
+ const why = "so this generation cannot be traced back to a reviewed commit";
8763
+ switch (gap) {
8764
+ case "no-commit": return `source: unspecified — knowledge/ is in a git repository with no commits yet, ${why}.\n fix: commit the record (git add knowledge && git commit) and re-run`;
8765
+ case "no-repo": return `source: unspecified — knowledge/ is not in a git repository, ${why}.\n fix: git init, commit the record, and re-run`;
8766
+ case "no-git": return `source: unspecified — git is not installed, ${why}.\n fix: install git, or pass --source-commit <sha> if the record is versioned elsewhere`;
8767
+ case "not-asked": return `source: unspecified — no knowledge directory was given, ${why}.`;
8768
+ }
8769
+ }
8708
8770
  function detectSourceCommit(knowledgeDir) {
8709
8771
  if (knowledgeDir === void 0) return "unspecified";
8710
8772
  try {
@@ -8890,7 +8952,7 @@ async function ingestCommand(args) {
8890
8952
  process.stdout.write(`ingest: unchanged — generation ${report.generation} already serves this corpus\n`);
8891
8953
  return 0;
8892
8954
  }
8893
- process.stdout.write(sourceCommit === "unspecified" ? "source: unspecified — knowledge/ is not in a git repository, so this generation cannot be traced back to a reviewed commit\n" : `source: ${sourceCommit}\n`);
8955
+ process.stdout.write(sourceCommit === "unspecified" ? provenanceNotice(provenanceGap(values.knowledge)) + "\n" : `source: ${sourceCommit}\n`);
8894
8956
  process.stdout.write(`ingest: generation ${report.generation} — ${report.nodes} nodes, ${report.chunks} chunks; embedded ${report.embedded}, carried ${report.carried}, failed ${report.failed}\n`);
8895
8957
  if (report.unsearchable > 0) {
8896
8958
  const pct = Math.round(report.unsearchable / Math.max(report.chunks, 1) * 100);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
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",