@panaversity/ksor 0.0.59 → 0.0.61

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,126 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.61
4
+
5
+ ### Patch Changes
6
+
7
+ - c8df2a0: `pnpm dev` now drops a document that is deleted or moved while it runs (#274).
8
+
9
+ The dev server keeps a staged copy of the record, and its watcher carried edits
10
+ and new documents into that copy but never removals. A document deleted during
11
+ a review went on answering 200 at its old url and stayed in the sidebar, and a
12
+ moved one was listed twice, once at each path, until the dev server restarted.
13
+ Nothing told the owner to restart.
14
+
15
+ The refresh now removes every staged file the record no longer holds, and every
16
+ folder that leaves empty, so the deleted or moved document answers 404 at its
17
+ old url and leaves the sidebar within about a second. A document whose audience
18
+ is edited so that the dev viewer may no longer read it leaves the same way.
19
+
20
+ Removals were held back because a 2026-08-18 measurement found that deleting a
21
+ staged file took the dev server down. On fumadocs-mdx 15.4.0 and Next 16.3.3
22
+ it recovers on its own, but the race behind it remains: Turbopack can compile
23
+ before fumadocs has regenerated the collection, so the dev log may show
24
+ `Module not found` for the removed file, and a request in that moment can
25
+ answer with a 500. Measured on a fresh scaffold, another page polled every 20ms
26
+ did so one to four times in 6 of 8 removals, then answered 200 again. Builds
27
+ are unaffected: they never run the watcher.
28
+
29
+ An existing project takes the fix with `ksor migrate --write-site`, which
30
+ reissues `system/site/lib/stage-knowledge.ts` with the rest of the site.
31
+
32
+ - 7d2e28a: A new npm or bun scaffold builds its site again.
33
+
34
+ `mdast-util-to-markdown@2.1.3`, published 2026-09-27, broke the MDX stringifier
35
+ of `fumadocs-core@16.15.4`, the version the scaffold pins. Any page with bold or
36
+ italic text sent it into recursion without end, so `npm run build` and
37
+ `bun run build` failed with `RangeError: Maximum call stack size exceeded`
38
+ (fuma-nama/fumadocs#3604). The npm and bun scaffolds ship no lockfile, so a
39
+ fresh install picked up 2.1.3. Their root `package.json` now has
40
+ `"overrides": { "mdast-util-to-markdown": "2.1.2" }`, and their README explains
41
+ it. The pnpm scaffold has not changed, because its committed lockfile already
42
+ holds 2.1.2.
43
+
44
+ If you scaffolded with npm or bun and your build now fails this way, add the
45
+ same `overrides` entry to your root `package.json` and install again. Remove the
46
+ entry when you move `fumadocs-core` to 16.15.15 or later, which has the
47
+ upstream fix.
48
+
49
+ ## 0.0.60
50
+
51
+ ### Patch Changes
52
+
53
+ - 2a7ef92: Five things a live walk of the published 0.0.59 found, fixed.
54
+
55
+ **`verify.mjs` reported ordinary markdown as an invented name.** Its name regex
56
+ let `\s+` cross a blank line, so a `## Meals` heading followed by a paragraph
57
+ opening `On travel…` was extracted as the name "Meals On" and reported as
58
+ changed-or-introduced. That fires on the first document an agent converts —
59
+ the check meant to make conversion trustworthy was crying wolf. Names are now
60
+ capitalised words on ONE line; a name the source never mentions is still caught.
61
+
62
+ **A scaffold followed verbatim published its first generation untraceably.**
63
+ `ksor init` runs `git init` and leaves zero commits, and its own epilogue went
64
+ install → dev → provision → refresh with no commit in between — so every
65
+ adopter's first publish said `source: unspecified` and skipped the R23
66
+ change-control check, which had no history to compare against. The epilogue now
67
+ says to commit before publishing, which is the whole fix.
68
+
69
+ **A plain build left a stale bundle tree unmentioned.** `ksor build` recomputes
70
+ every `bundles[].sha256` in the lock but only `--bundles` writes the directory,
71
+ so after one `--bundles` run and any ordinary build the lock claimed a digest
72
+ nothing on disk produced, while the tree that exists to be SENT somewhere aged
73
+ silently. A plain build now says which build the directory came from and how to
74
+ refresh it. Reported, never deleted: it is the adopter's output and may be
75
+ mid-handover.
76
+
77
+ **Tutorial 01's first build block was one line short.** R23 landed the day after
78
+ that walk, so the shipped block omitted `change-control: not checked` on a page
79
+ whose headline claim is that every output was pasted as it appeared. Re-captured
80
+ on 0.0.59, with a paragraph on what both honesty lines mean and when they go.
81
+
82
+ **The dev server's `/llms.txt` does not change after an approval.** Not a bug and
83
+ not fixable in the route: `output: "export"` requires a static route handler, so
84
+ `pnpm dev` computes it once per process while the document's page beside it
85
+ updates. Stated in the emitted README's troubleshooting table and in the tutorial
86
+ step whose own prompt is "Why isn't my refund policy in llms.txt?" — verified by
87
+ testing both alternatives, each of which breaks the export.
88
+
89
+ - 6c47618: Measure WHICH skill a real agent reaches for, and record what the first sweeps
90
+ found.
91
+
92
+ A live walk of the published 0.0.59 reported that `add-sources` did not fire on
93
+ its headline prompt. The tempting repair is to reword the description until it
94
+ does, which is a guess. This is the instrument that replaces the guess: N runs
95
+ per phrase, in a fresh scaffold with all three skills present, graded on which
96
+ skill the agent actually invoked, across more than one model. Reported, never
97
+ gating — a model is stochastic and a threshold over a handful of runs flakes.
98
+
99
+ **The model is a column, because the answer depends on it.** Same phrase, same
100
+ scaffold, same harness: `claude-sonnet-5` fired `add-sources` 3/3 where
101
+ `claude-opus-5` fired nothing 0/2. The walk used the CLI default and the first
102
+ probe pinned Sonnet, which is why they disagreed — neither was wrong, and
103
+ neither alone measured the trigger.
104
+
105
+ **The finding is narrower than the walk suggested.** `add-sources` fires on four
106
+ of five phrases on both models, and both controls behave: a different skill wins
107
+ the intake phrase, and nothing fires on a question about the repo itself. It
108
+ misses exactly one shape on Opus — the owner pointing at a file already in the
109
+ repo and naming a destination.
110
+
111
+ **And the obvious repair does not work.** Naming that shape in the description
112
+ was tried and measured: unchanged at 0/3. So the cause is not the wording — an
113
+ instruction concrete enough to act on gets acted on, and no skill is consulted.
114
+ The clause was reverted rather than kept, because it is resident context in
115
+ every session and bought nothing a measurement can see. The negative result is
116
+ recorded beside the rows it explains.
117
+
118
+ Also fixed while building it: the CLI can emit raw control characters inside its
119
+ JSON, which `JSON.parse` rejects outright — the harness lost the whole transcript
120
+ to a stray byte. It now falls back to a scrubbed parse.
121
+
122
+ Test infrastructure only; nothing an adopter installs behaves differently.
123
+
3
124
  ## 0.0.59
4
125
 
5
126
  ### Patch Changes
package/dist/cli.mjs CHANGED
@@ -12216,7 +12216,7 @@ function runBuild(args, cwd, io, options) {
12216
12216
  io.out(`ksor build: ${lock.documents.length} document(s), ${admitted} admitted to a machine surface at ${lock.as_of}\n${provenanceLine({
12217
12217
  ...facts,
12218
12218
  dirty: lock.dirty
12219
- }, root)}\n` + (change.notice === null ? "" : ` ${change.notice}\n`) + notice + `${pendingIndexes.map((w) => ` wrote ${w}\n`).join("")}${staleIndexes.map((r) => ` removed ${r} (its directory earns no index)\n`).join("")}` + (parsed.bundles ? bundlesReport(bundles, viewers) : "") + ` wrote build.lock.json — build_id ${lock.build_id}\n`);
12219
+ }, root)}\n` + (change.notice === null ? "" : ` ${change.notice}\n`) + notice + `${pendingIndexes.map((w) => ` wrote ${w}\n`).join("")}${staleIndexes.map((r) => ` removed ${r} (its directory earns no index)\n`).join("")}` + (parsed.bundles ? bundlesReport(bundles, viewers) : staleBundlesNotice(root, lock.build_id)) + ` wrote build.lock.json — build_id ${lock.build_id}\n`);
12220
12220
  return 0;
12221
12221
  }
12222
12222
  /**
@@ -12241,6 +12241,35 @@ function writeBundles(root, bundles, lockText) {
12241
12241
  }
12242
12242
  writeFileSync(path.join(out, LOCK_NAME), lockText);
12243
12243
  }
12244
+ /**
12245
+ * `.ksor/out/bundles/` from an EARLIER build, when this run did not write it.
12246
+ *
12247
+ * A plain `ksor build` recomputes every `bundles[].sha256` in the root lock —
12248
+ * they are a function of what `build_id` already hashes — but it does not
12249
+ * touch the directory, which only `--bundles` writes. So after one
12250
+ * `--bundles` run and any ordinary build, the root lock claims a digest that
12251
+ * nothing on disk produces, and the tree that exists to be SENT somewhere goes
12252
+ * on aging with nothing said (found by walking the published 0.0.59).
12253
+ *
12254
+ * Reported, never deleted and never refused: the directory is the adopter's
12255
+ * output, its co-located lock still describes its own bytes coherently, and a
12256
+ * build that removed it would destroy something an adopter may be mid-way
12257
+ * through handing over. Honest absence, never silent weakness.
12258
+ */
12259
+ function staleBundlesNotice(root, buildId) {
12260
+ const beside = path.join(root, BUNDLES_DIR, LOCK_NAME);
12261
+ if (!existsSync(beside)) return "";
12262
+ let wrote;
12263
+ try {
12264
+ wrote = JSON.parse(readFileSync(beside, "utf8")).build_id;
12265
+ } catch {
12266
+ wrote = "unreadable";
12267
+ }
12268
+ if (wrote === buildId) return "";
12269
+ return ` bundles: ${BUNDLES_DIR}/ is from build ${wrote}, not this one — the lock records this build's digests, so what is on disk no longer matches them
12270
+ fix: re-run with \`--bundles\` before sending them, or delete the directory
12271
+ `;
12272
+ }
12244
12273
  /** One line per bundle written, and a line per link it carries to a concept it excludes. */
12245
12274
  function bundlesReport(bundles, viewers) {
12246
12275
  let text = "";
@@ -12308,6 +12337,20 @@ const SCRIPT_BODIES = {
12308
12337
  }
12309
12338
  };
12310
12339
  /**
12340
+ * Versions of dependencies of dependencies that npm and bun must be told to
12341
+ * hold, written as `overrides`, which both read. pnpm needs none here: its
12342
+ * committed lockfile already holds each one.
12343
+ *
12344
+ * mdast-util-to-markdown 2.1.3 (2026-09-27) writes bold and italic only
12345
+ * through a handler's `attention`. The MDX stringifier of the fumadocs-core
12346
+ * 16.15.4 that the site pins wraps every handler without it, so the site
12347
+ * build recursed until the stack overflowed (issue #276,
12348
+ * fuma-nama/fumadocs#3604, fixed in fumadocs-core 16.15.15). Remove the entry,
12349
+ * and the README paragraph that explains it, in the change that moves the site
12350
+ * to that Fumadocs: init-manager.integration.test.ts fails until they are gone.
12351
+ */
12352
+ const OVERRIDES = { "mdast-util-to-markdown": "2.1.2" };
12353
+ /**
12311
12354
  * Rewrite the scaffold's root package.json for the manager. Structured — a
12312
12355
  * JSON transform, never string surgery — because the manifest is the one
12313
12356
  * file where a half-applied spelling map would still parse and then lie.
@@ -12322,7 +12365,8 @@ function transformManifest(source, manager) {
12322
12365
  ...parsed.scripts,
12323
12366
  ...SCRIPT_BODIES[manager]
12324
12367
  },
12325
- workspaces: [...WORKSPACE_GLOBS]
12368
+ workspaces: [...WORKSPACE_GLOBS],
12369
+ overrides: { ...OVERRIDES }
12326
12370
  };
12327
12371
  return `${JSON.stringify(out, null, 2)}\n`;
12328
12372
  }
@@ -14420,6 +14464,9 @@ function handoff(io, name, targetWasDot, manager) {
14420
14464
  io.out(`${name} is ready — your knowledge, your repo, yours outright.\n
14421
14465
  Next (or just tell your coding agent to take it from here):
14422
14466
  ` + enter + ` ${install}\n ${run("dev").padEnd(15)} # the site, live at http://localhost:3000\n
14467
+ Commit before you publish — a build traces to a reviewed commit:
14468
+ git add -A && git commit -m 'Scaffold the record'
14469
+
14423
14470
  Then, for the agent surface (needs Postgres and a provider key):
14424
14471
  ${run("provision").padEnd(15)} # once: copy .env.example to .env and set KSOR_DB_URL,\n # then apply the schema
14425
14472
  ${run("refresh").padEnd(15)} # PUBLISH the record — ingest knowledge/ into a generation\n ${run("serve").padEnd(15)} # the MCP server, over what you just published\n
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
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",
@@ -38,7 +38,12 @@ const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, "").replace(/\[\^[^\]]+\]:?/g
38
38
 
39
39
  const tokens = new Set();
40
40
  for (const m of body.matchAll(/\d[\d,.:/-]*\d|\d/g)) tokens.add(m[0]);
41
- for (const m of body.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b/g)) tokens.add(m[0]);
41
+ // A name is capitalised words on ONE line. `\s+` here would cross a blank
42
+ // line, so a `## Meals` heading followed by a paragraph opening `On travel…`
43
+ // was extracted as the name "Meals On" and reported as invented — a false
44
+ // positive on ordinary markdown, hit on the first document an agent writes
45
+ // (found by walking the published 0.0.59).
46
+ for (const m of body.matchAll(/\b[A-Z][a-z]+(?:[^\S\r\n]+[A-Z][a-z]+)+\b/g)) tokens.add(m[0]);
42
47
 
43
48
  const missing = [...tokens].filter((t) => !extraction.includes(fold(t))).sort();
44
49
  for (const t of missing) console.log(t);
@@ -38,7 +38,12 @@ const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, "").replace(/\[\^[^\]]+\]:?/g
38
38
 
39
39
  const tokens = new Set();
40
40
  for (const m of body.matchAll(/\d[\d,.:/-]*\d|\d/g)) tokens.add(m[0]);
41
- for (const m of body.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b/g)) tokens.add(m[0]);
41
+ // A name is capitalised words on ONE line. `\s+` here would cross a blank
42
+ // line, so a `## Meals` heading followed by a paragraph opening `On travel…`
43
+ // was extracted as the name "Meals On" and reported as invented — a false
44
+ // positive on ordinary markdown, hit on the first document an agent writes
45
+ // (found by walking the published 0.0.59).
46
+ for (const m of body.matchAll(/\b[A-Z][a-z]+(?:[^\S\r\n]+[A-Z][a-z]+)+\b/g)) tokens.add(m[0]);
42
47
 
43
48
  const missing = [...tokens].filter((t) => !extraction.includes(fold(t))).sort();
44
49
  for (const t of missing) console.log(t);
@@ -94,6 +94,15 @@ never picks up a day-zero compromised release. bun has no equivalent — its
94
94
  default refusal of dependency install scripts covers the OTHER half of that
95
95
  posture, and this sentence is the disclosure.
96
96
 
97
+ <!-- /ksor:pm -->
98
+ <!-- ksor:pm npm bun -->
99
+
100
+ `package.json` also holds one dependency of a dependency back: `overrides`
101
+ keeps `mdast-util-to-markdown` at 2.1.2. With the `fumadocs-core` this scaffold
102
+ pins, version 2.1.3 makes the site build recurse until it runs out of stack
103
+ ([fumadocs#3604](https://github.com/fuma-nama/fumadocs/issues/3604)). Delete the
104
+ override when you move `fumadocs-core` to 16.15.15 or later, which has the fix.
105
+
97
106
  <!-- /ksor:pm -->
98
107
 
99
108
  ---
@@ -724,6 +733,7 @@ map rather than a substitute.
724
733
  | the agent answers questions 2 and 3 instead of declining | no floor is measured, so the gate is off (`abstain OFF`, `gate: "off"`) — step 3's `calibrate` was skipped | `pnpm exec ksor calibrate --instance instance.md`, paste the block, restart |
725
734
  | a deployed door serves an empty record | deploying does not publish — and a laptop DSN is unreachable from the host | point both at one hosted Postgres, then `pnpm refresh` |
726
735
  | the home page and `/llms.txt` are empty | every document is still a draft — correct, not broken | approve one and rebuild |
736
+ | `/llms.txt` on the DEV server does not change after you approve | `output: "export"` requires its route handler to be static, so `pnpm dev` computes it once per process — the document page beside it updates, that file does not | restart `pnpm dev`; the built site is always current, so this is a dev-server artefact only |
727
737
  | `ksor-record-empty` | every document was deleted — a record is never empty, so nothing was written | add one document of your own (or restore one from git) before deleting the last starter |
728
738
  | `ksor-approver-unauthorised` | a document is approved by an actor `.ksor/governance.yaml` no longer names — usually `human:you` after the interview | re-attribute the approval to your handle, or restore the actor to `approval_authorities` |
729
739
  | `ksor-generated-stale` at `pnpm build` or `pnpm refresh` | a `stable` document's body changed since a commit where it was stable, and `generated.at` was not advanced past that commit's (left alone, or moved backward) — the stamp dates the text, and `pnpm check` reads no document history, so it passed | set `generated.at` to an instant after the edit and re-approve (`ksor.approval.at` may not precede it) |
@@ -672,15 +672,15 @@ function fillStage(recordDir: string, stageDir: string, development: boolean): v
672
672
  }
673
673
 
674
674
  /**
675
- * Dev only: carry edits AND ARRIVALS into the stage, so `pnpm dev` shows the
676
- * record as the owner is writing it rather than as it stood when the server
677
- * started — the regenerated indexes included, so a retitled document is
675
+ * Dev only: carry edits, ARRIVALS and REMOVALS into the stage, so `pnpm dev`
676
+ * shows the record as the owner is writing it rather than as it stood when the
677
+ * server started — the regenerated indexes included, so a retitled document is
678
678
  * retitled in its folder's listing too.
679
679
  *
680
- * Adds and edits — never removals. The 2026-08-18 measurement this refused
681
- * adds on ("fumadocs' own watcher cannot see a dot-prefixed collection
682
- * directory") no longer holds: on fumadocs-mdx 15.3.0 a file written into
683
- * `.staged-knowledge` DOES regenerate the collection, twice-observed as
680
+ * Arrivals first. The 2026-08-18 measurement this refused adds on ("fumadocs'
681
+ * own watcher cannot see a dot-prefixed collection directory") no longer
682
+ * holds: on fumadocs-mdx 15.3.0 a file written into `.staged-knowledge` DOES
683
+ * regenerate the collection, twice-observed as
684
684
  * `[MDX] generated files` in the dev log. What actually kept a new document
685
685
  * off every surface was this function, which walked the STAGE and skipped
686
686
  * anything the stage did not already hold — so a plan entry with no file on
@@ -692,11 +692,23 @@ function fillStage(recordDir: string, stageDir: string, development: boolean): v
692
692
  * this way before the stage existed (0.0.40 serves an added document at 200),
693
693
  * so this is a regression repaired rather than a feature.
694
694
  *
695
- * REMOVALS still wait for the restart `pnpm dev` already needs for
696
- * instance.md: the same measurement found a deleted file leaves fumadocs'
697
- * generated imports pointing at something gone, which takes the dev server
698
- * down rather than showing a stale page. An arrival has no such failure mode
699
- * — nothing points at a file that has only just appeared.
695
+ * REMOVALS used to wait for a restart: the same 2026-08-18 measurement found a
696
+ * deleted file left fumadocs' generated imports pointing at something gone,
697
+ * which took the dev server down. On fumadocs-mdx 15.4.0 and Next 16.3.3 it
698
+ * does not stay down. Measured 2026-10-01 on a fresh scaffold: each document
699
+ * deleted or moved while `pnpm dev` ran regenerated the collection (`[MDX]
700
+ * generated files`), answered 404 at its old url within about a second and
701
+ * left the sidebar, and every other page answered 200 with no restart.
702
+ *
703
+ * What remains is a race, not an outage. Turbopack can compile
704
+ * `.source/server.ts` before fumadocs has rewritten it, so the log shows
705
+ * `Module not found` for the removed file and a request in that window answers
706
+ * 500: another page, polled every 20ms, did so one to four times in 6 of 8
707
+ * removals, then answered 200 again. Both watchers react to the same unlink,
708
+ * so nothing here can order them. Errors that clear themselves are still the
709
+ * better failure: before this, a deleted document went on serving at 200 and
710
+ * stayed in the sidebar, and a moved one was listed twice, until someone
711
+ * thought to restart (issue #274).
700
712
  */
701
713
  function refreshStage(recordDir: string, stageDir: string): void {
702
714
  // Under the lock like every other write here: a save landing while another
@@ -719,6 +731,9 @@ function refreshStage(recordDir: string, stageDir: string): void {
719
731
  mkdirSync(path.dirname(staged), { recursive: true });
720
732
  writeFileSync(staged, bytes);
721
733
  }
734
+ // Removals from the plan too: a staged file the plan no longer holds was
735
+ // deleted, moved, or withdrawn from this viewer, and is not the record.
736
+ pruneExcept(stageDir, new Set(plan.entries.map((e) => path.resolve(stageDir, e.rel))));
722
737
  writeManifest(stageDir, plan.manifest);
723
738
  });
724
739
  }
@@ -867,6 +882,14 @@ function publishSims(sourceDir: string): void {
867
882
  * nothing else writes it), so what is not published now does not belong.
868
883
  */
869
884
  function pruneSims(target: string, published: ReadonlySet<string>): void {
885
+ pruneExcept(target, published);
886
+ }
887
+
888
+ /**
889
+ * Every file under `target` whose resolved path `keep` does not hold, removed,
890
+ * and every directory that leaves empty. `target` itself always stays.
891
+ */
892
+ function pruneExcept(target: string, keep: ReadonlySet<string>): void {
870
893
  const walk = (dir: string): boolean => {
871
894
  let entries;
872
895
  try {
@@ -882,7 +905,7 @@ function pruneSims(target: string, published: ReadonlySet<string>): void {
882
905
  else empty = false;
883
906
  continue;
884
907
  }
885
- if (published.has(path.resolve(here))) {
908
+ if (keep.has(path.resolve(here))) {
886
909
  empty = false;
887
910
  continue;
888
911
  }