@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 +121 -0
- package/dist/cli.mjs +49 -2
- package/package.json +1 -1
- package/templates/scaffold/.agents/skills/add-sources/verify.mjs +6 -1
- package/templates/scaffold/.claude/skills/add-sources/verify.mjs +6 -1
- package/templates/scaffold/README.md +10 -0
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +36 -13
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) :
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
676
|
-
* record as the owner is writing it rather than as it stood when the
|
|
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
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
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
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
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 (
|
|
908
|
+
if (keep.has(path.resolve(here))) {
|
|
886
909
|
empty = false;
|
|
887
910
|
continue;
|
|
888
911
|
}
|