@panaversity/ksor 0.0.41 → 0.0.43
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 +116 -0
- package/dist/cli.mjs +1 -1
- package/docs/deploying.md +20 -0
- package/docs/ingesting.md +1 -1
- package/package.json +1 -1
- package/templates/scaffold/AGENTS.md +3 -3
- package/templates/scaffold/env.example +3 -2
- package/templates/scaffold/instance.md +10 -7
- package/templates/scaffold/knowledge/surfaces/index.md +1 -1
- package/templates/scaffold/knowledge/surfaces/overview.md +1 -1
- package/templates/scaffold/system/site/components/governance.tsx +113 -85
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +37 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,121 @@
|
|
|
1
1
|
# @panaversity/ksor
|
|
2
2
|
|
|
3
|
+
## 0.0.43
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9ecfd78: **A document page that reads at a glance, and a dev server that sees new
|
|
8
|
+
documents again.**
|
|
9
|
+
|
|
10
|
+
The governance row is two tiers. It was one line carrying 79 characters, of
|
|
11
|
+
which the approver was 32 (41%) and the three labels 19 (24%) — so a producer
|
|
12
|
+
id was the longest thing on the page and the two facts a reader actually scans
|
|
13
|
+
for, what state this is in and whether anyone has checked it, competed with it.
|
|
14
|
+
Now the chips lead with Export beside them, and provenance sits beneath in
|
|
15
|
+
muted weight. Nothing is hidden: decision 21 requires a governance act to name
|
|
16
|
+
its actor and decision 27 requires a non-human approver to be disclosed, so the
|
|
17
|
+
approver moved one line down, not one click away, and every byte of it is still
|
|
18
|
+
in the server-rendered markup an agent parses.
|
|
19
|
+
|
|
20
|
+
Export no longer lands in the middle of the row. It and the reading time each
|
|
21
|
+
carried their own `ms-auto`, and on a row narrow enough to wrap they shared a
|
|
22
|
+
line — where two auto margins SPLIT the free space rather than stacking. They
|
|
23
|
+
are one right-hand cluster now.
|
|
24
|
+
|
|
25
|
+
**And adding a document to `knowledge/` while `pnpm dev` runs shows it again.**
|
|
26
|
+
This regressed in 0.0.41: `refreshStage` walked the STAGE and skipped anything
|
|
27
|
+
the stage did not already hold, so an arrival — which has no file to walk onto
|
|
28
|
+
— was never written, and the manifest naming what publishes never learned about
|
|
29
|
+
it. Measured: `/docs/<new>/` 404 → 200, sidebar 0 → 1, `llms.txt` 0 → 1;
|
|
30
|
+
0.0.40 served it at 200, so this is a repair rather than a feature. Removals
|
|
31
|
+
still wait for a restart, deliberately — a deleted file leaves fumadocs'
|
|
32
|
+
generated imports pointing at something gone.
|
|
33
|
+
|
|
34
|
+
The comment explaining why arrivals were refused was also wrong, and is
|
|
35
|
+
corrected: fumadocs-mdx 15.3.0 DOES regenerate on a write into the
|
|
36
|
+
dot-prefixed stage. Our own function was the blocker.
|
|
37
|
+
|
|
38
|
+
Finally, the starter's `knowledge/surfaces/overview.md` was titled `Surfaces`
|
|
39
|
+
inside the Surfaces section, so its breadcrumb read `Surfaces › Surfaces`. It
|
|
40
|
+
is `Overview` now.
|
|
41
|
+
|
|
42
|
+
- 9ecfd78: **Two fixes on the document page.**
|
|
43
|
+
|
|
44
|
+
The governance row put **Export** in the middle of the row with nothing under
|
|
45
|
+
it. Export and the reading time each carried their own `ms-auto`, and on a row
|
|
46
|
+
narrow enough to wrap they landed on the same line — where two auto margins
|
|
47
|
+
SPLIT the free space between them rather than stacking, so Export came to rest
|
|
48
|
+
mid-row instead of at either end. They are now one right-hand cluster and
|
|
49
|
+
travel together at every width.
|
|
50
|
+
|
|
51
|
+
And the starter's `knowledge/surfaces/overview.md` was titled `Surfaces` inside
|
|
52
|
+
a section already called Surfaces, so its breadcrumb read `Surfaces › Surfaces`
|
|
53
|
+
and the sidebar showed a Surfaces inside Surfaces. It is titled `Overview` now,
|
|
54
|
+
matching its filename, and the generated section index was regenerated with it.
|
|
55
|
+
|
|
56
|
+
## 0.0.42
|
|
57
|
+
|
|
58
|
+
### Patch Changes
|
|
59
|
+
|
|
60
|
+
- d55097c: **Document what a deploy actually does to your lock.** `vercel.json` builds the
|
|
61
|
+
site with `pnpm build`, which runs `ksor build` first — so the host regenerates
|
|
62
|
+
every `index.md` and `build.lock.json` before building. That has two
|
|
63
|
+
consequences worth knowing, and neither was written down: you can deploy
|
|
64
|
+
without ever running `ksor build` yourself, and the `build.lock.json` in your
|
|
65
|
+
repository is not necessarily the one that shipped.
|
|
66
|
+
|
|
67
|
+
Nothing changes in behaviour. The record checker still runs on the deploy, so a
|
|
68
|
+
record that breaks the profile still fails there, and the `build_id` that did
|
|
69
|
+
ship is stamped into the deployed `llms.txt`.
|
|
70
|
+
|
|
71
|
+
`docs/deploying.md` now also shows the stricter posture for adopters who want
|
|
72
|
+
the deployed build reviewed before it ships — `buildCommand: "pnpm -C
|
|
73
|
+
system/site build"`, which refuses `ksor-lock-missing` or `ksor-lock-stale`
|
|
74
|
+
until someone runs `ksor build` and commits it. That is one line in your own
|
|
75
|
+
`vercel.json`; ksor ships no flag for it.
|
|
76
|
+
|
|
77
|
+
- abef414: **Fix the release-note lookup, properly this time.** The previous release added
|
|
78
|
+
`releaseNote()` so doc-truth assertions could survive a changeset being folded
|
|
79
|
+
into the changelog. It resolved a consumed note to the NEWEST changelog
|
|
80
|
+
section, which is only correct for the release that consumes it: a note
|
|
81
|
+
consumed in 0.0.41 lives in the 0.0.41 section forever, so by 0.0.42 the lookup
|
|
82
|
+
returned a different release entirely.
|
|
83
|
+
|
|
84
|
+
Two failures came out of that, and the second was worse than the bug it
|
|
85
|
+
replaced: presence assertions went red, and a fenced-block scan went VACUOUS —
|
|
86
|
+
passing because the section handed to it contained no code blocks at all.
|
|
87
|
+
|
|
88
|
+
`releaseNote()` now returns the whole changelog once a note is consumed, plus
|
|
89
|
+
whether the note is still `pending`. Assertions about the PRESENCE of prose use
|
|
90
|
+
the text (finding it anywhere in the changelog proves it shipped); assertions
|
|
91
|
+
about STRUCTURE gate on `pending`, because "every fenced block must show
|
|
92
|
+
`--approve-by`" is a rule about a note still under review, not one to apply to
|
|
93
|
+
the whole published history.
|
|
94
|
+
|
|
95
|
+
Verified in both states and mutation-tested against the released tree: removing
|
|
96
|
+
`--approve-by` or changing the tool-size figure in the changelog turns the
|
|
97
|
+
assertions red.
|
|
98
|
+
|
|
99
|
+
- bfcf900: **A new record already names its DSN variable.** `instance.md`'s
|
|
100
|
+
`database.dsn_env` shipped commented out, so climbing to the served rung began
|
|
101
|
+
with an edit whose only purpose was to delete two `#` characters — and the
|
|
102
|
+
instruction to do it was repeated in four places, one of which (`.env.example`)
|
|
103
|
+
sat right beside a `KSOR_DB_URL=` line that was NOT commented. A first-time
|
|
104
|
+
reader had to notice that one file names the variable and another defines it,
|
|
105
|
+
and that only one of the two needed uncommenting.
|
|
106
|
+
|
|
107
|
+
It is filled in now. Naming an environment variable costs nothing and requires
|
|
108
|
+
no database: `pnpm dev` and `pnpm build` never read it, and the value only has
|
|
109
|
+
to exist when you run `provision`, `refresh` or `serve`. Verified on a real
|
|
110
|
+
scaffold from the published package with the block live and `KSOR_DB_URL`
|
|
111
|
+
unset — `check`, `ksor build` and a full static site build all succeed, and the
|
|
112
|
+
record publishes.
|
|
113
|
+
|
|
114
|
+
So the served rung is now: set `KSOR_DB_URL` in `.env`, then `provision`,
|
|
115
|
+
`refresh`, `serve`. The step that was pure ceremony is gone, and `ksor init`'s
|
|
116
|
+
own next-steps, the scaffold's `AGENTS.md`, `.env.example` and
|
|
117
|
+
`docs/ingesting.md` all say the same thing.
|
|
118
|
+
|
|
3
119
|
## 0.0.41
|
|
4
120
|
|
|
5
121
|
### Patch Changes
|
package/dist/cli.mjs
CHANGED
|
@@ -13324,7 +13324,7 @@ function handoff(io, name, targetWasDot, manager) {
|
|
|
13324
13324
|
Next (or just tell your coding agent to take it from here):
|
|
13325
13325
|
` + enter + ` ${install}\n ${run("dev").padEnd(15)} # the site, live at http://localhost:3000\n
|
|
13326
13326
|
Then, for the agent surface (needs Postgres and a provider key):
|
|
13327
|
-
${run("provision").padEnd(15)} # once:
|
|
13327
|
+
${run("provision").padEnd(15)} # once: copy .env.example to .env and set KSOR_DB_URL,\n # then apply the schema
|
|
13328
13328
|
${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
|
|
13329
13329
|
` + pnpmHint + "Start in knowledge/ — AGENTS.md carries the working rules.\n");
|
|
13330
13330
|
}
|
package/docs/deploying.md
CHANGED
|
@@ -254,6 +254,26 @@ and reaches the site at its next build (it reads the ledger), so the act that
|
|
|
254
254
|
withdraws a document is the same merged commit on both surfaces. Merge the
|
|
255
255
|
ledger entry, rebuild, redeploy.
|
|
256
256
|
|
|
257
|
+
**Your deploy runs it too, and that is deliberate.** `vercel.json` builds with
|
|
258
|
+
`pnpm build`, so the host regenerates the indexes and the lock before building
|
|
259
|
+
the site. The consequence is worth knowing in both directions: you can deploy
|
|
260
|
+
without ever having run `ksor build` yourself — the record checker still runs
|
|
261
|
+
there, so a record that breaks the profile still fails the deploy — but the
|
|
262
|
+
`build.lock.json` in your repository is not necessarily the one that shipped.
|
|
263
|
+
The `build_id` that DID ship is stamped into the deployed `llms.txt`.
|
|
264
|
+
|
|
265
|
+
If you want the stricter property — the deployed build_id is one a human
|
|
266
|
+
reviewed in a pull request — build the site alone instead:
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
"buildCommand": "pnpm -C system/site build"
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The site refuses `ksor-lock-missing` or `ksor-lock-stale` when the committed
|
|
273
|
+
lock does not describe the tree, so a deploy then fails until someone runs
|
|
274
|
+
`ksor build` and commits the result. That is your file to change; ksor does not
|
|
275
|
+
choose it for you.
|
|
276
|
+
|
|
257
277
|
## Keeping people out of the site
|
|
258
278
|
|
|
259
279
|
The door has auth ([authorization.md](./authorization.md)). The **site** is
|
package/docs/ingesting.md
CHANGED
|
@@ -25,7 +25,7 @@ created for you.
|
|
|
25
25
|
| **The corpus** | `knowledge/` at your repo root — CommonMark `.md`, one document per file, in the KSoR Profile of OKF: `type`, `title`, `description`, `status` and `ksor.audience` in frontmatter | `pnpm check` validates it and explains any violation; `ksor build` must have written a current `build.lock.json` before ingest will run |
|
|
26
26
|
| **The database** | Postgres with **pgvector** — `CREATE EXTENSION vector;` | any managed host; the DDL below needs a role that can create tables |
|
|
27
27
|
| **The provider key** | `GEMINI_API_KEY` — the default embedding provider is `gemini-embedding-001` | [aistudio.google.com](https://aistudio.google.com/apikey); the free tier covers a first corpus |
|
|
28
|
-
| **The DSN** | `KSOR_DB_URL`, named by `instance.md`'s `database.dsn_env` |
|
|
28
|
+
| **The DSN** | `KSOR_DB_URL`, named by `instance.md`'s `database.dsn_env` | already named by `instance.md`'s `database:` block |
|
|
29
29
|
|
|
30
30
|
Both variables go in `.env` beside `instance.md` — `ksor` reads it automatically,
|
|
31
31
|
and `.env` is gitignored. Every command below is run **from your repository
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panaversity/ksor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.43",
|
|
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",
|
|
@@ -60,9 +60,9 @@ pnpm check # the format checker — run before handing off any knowledge c
|
|
|
60
60
|
with honest abstention. It is the climbed rung — not required for `pnpm dev`.
|
|
61
61
|
Stand it up in this order (each step's errors explain how to fix themselves):
|
|
62
62
|
|
|
63
|
-
1. **
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
1. **Check `instance.md`.** The one required block is already filled in — it
|
|
64
|
+
names the environment variable holding your DSN (never the DSN itself), and
|
|
65
|
+
there is nothing to do here unless you want a different variable name:
|
|
66
66
|
|
|
67
67
|
```yaml
|
|
68
68
|
database:
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# A real environment variable always wins over this file, so CI and production
|
|
3
3
|
# keep their own values.
|
|
4
4
|
|
|
5
|
-
# The Postgres store
|
|
6
|
-
# `database
|
|
5
|
+
# The Postgres store. instance.md already names this variable in its
|
|
6
|
+
# `database.dsn_env`, so setting it here is the whole of the wiring.
|
|
7
|
+
# Needs the pgvector extension:
|
|
7
8
|
# CREATE EXTENSION vector;
|
|
8
9
|
# On a remote host prefer sslmode=verify-full, which states the TLS posture
|
|
9
10
|
# explicitly instead of relying on a driver default that is due to change.
|
|
@@ -6,17 +6,20 @@ description: What a Knowledge System of Record is, how a project climbs the gove
|
|
|
6
6
|
toolchain:
|
|
7
7
|
requires: ">=KSOR-STAMP-VERSION"
|
|
8
8
|
scaffolded: "KSOR-STAMP-VERSION"
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
9
|
+
# `database.dsn_env` names the environment variable holding your Postgres DSN —
|
|
10
|
+
# never the DSN itself, which belongs in .env. It is filled in because naming a
|
|
11
|
+
# variable costs nothing and needs no database: `pnpm dev` and `pnpm build` do
|
|
12
|
+
# not read it, and the value only has to exist when you climb to the served
|
|
13
|
+
# rung. To climb: copy .env.example to .env and set KSOR_DB_URL, then
|
|
14
|
+
# `pnpm provision` once (schema + grant), then `pnpm refresh` to PUBLISH the
|
|
15
|
+
# record, then `pnpm serve`. Serving does not publish — that is deliberate, and
|
|
16
|
+
# skipping refresh serves nothing.
|
|
14
17
|
# Nothing else here is required:
|
|
15
18
|
# `embedding:` already defaults to Gemini at 1536 dimensions, and leaving
|
|
16
19
|
# `retrieval:` out starts you with the abstention gate off and honest about it
|
|
17
20
|
# (turn it on afterwards with `ksor calibrate`, once the record is serving).
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
database:
|
|
22
|
+
dsn_env: KSOR_DB_URL
|
|
20
23
|
# Where agents reach this record's MCP surface, and the semver it publishes as.
|
|
21
24
|
# Both go into /.well-known/mcp/server.json, the document an agent reads to
|
|
22
25
|
# DISCOVER this record instead of being told the URL. Leave mcp_url out until
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Surfaces
|
|
2
2
|
|
|
3
|
-
* [
|
|
3
|
+
* [Overview](overview.md) - One source, published through several synchronized projections.
|
|
4
4
|
* [The human surface](for-people.md) - Pages for reading, reviewing and sharing the record.
|
|
5
5
|
* [The agent surface](for-agents.md) - MCP for retrieval with citations, and machine-readable files beside it.
|
|
@@ -216,104 +216,132 @@ export function GovernanceMeta({
|
|
|
216
216
|
// tier, `unverified` included, and that is the whole point of printing it.
|
|
217
217
|
// The early return this replaced would have hidden the tier on exactly the
|
|
218
218
|
// documents whose tier is the only governance fact they have.
|
|
219
|
+
/**
|
|
220
|
+
* TWO TIERS, because the row's length was never its vocabulary.
|
|
221
|
+
*
|
|
222
|
+
* Measured on the starter's own document: 79 characters, of which the
|
|
223
|
+
* APPROVER is 32 (41%) and the three labels 19 (24%). One producer id was
|
|
224
|
+
* longer than both governance chips together, so the two facts a reader
|
|
225
|
+
* scans for — what state is this in, and has anyone checked it — competed
|
|
226
|
+
* with a string that means nothing to them, and Export was pushed onto a
|
|
227
|
+
* line of its own.
|
|
228
|
+
*
|
|
229
|
+
* So the chips lead and the provenance follows beneath them. Nothing is
|
|
230
|
+
* hidden: decision 21 says a governance act NAMES its actor and decision
|
|
231
|
+
* 27's starter revision requires a non-human approver to be DISCLOSED, so
|
|
232
|
+
* demoting `ksor.approval` to a hover would trade a governance guarantee
|
|
233
|
+
* for a tidier row (critical rule 1). It is one line lower, not one click
|
|
234
|
+
* away, and every byte of it is still in the server-rendered markup an
|
|
235
|
+
* agent parses.
|
|
236
|
+
*/
|
|
219
237
|
return (
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
238
|
+
<div className="mb-7 border-b border-fd-border pb-4">
|
|
239
|
+
<dl className="flex flex-wrap items-baseline gap-x-8 gap-y-2.5">
|
|
240
|
+
{state === null && alsoBadge === null ? null : (
|
|
241
|
+
<Fact label="Status">
|
|
242
|
+
<span className="flex flex-wrap items-baseline gap-1.5">
|
|
243
|
+
{state === null ? null : <Chip text={state} tone={statusTone(status)} />}
|
|
244
|
+
{alsoBadge === null ? null : (
|
|
245
|
+
<BadgeChip badge={alsoBadge} effectiveFrom={effectiveFrom} />
|
|
246
|
+
)}
|
|
247
|
+
</span>
|
|
248
|
+
</Fact>
|
|
249
|
+
)}
|
|
250
|
+
{/* The tier OKF's own vocabulary names, on every document including the
|
|
232
251
|
unverified ones — that is the honest state of a stable, approved
|
|
233
252
|
concept nobody has reviewed, and hiding it would leave a reader unable
|
|
234
253
|
to tell "checked" from "never mentioned" (research/okf-native.md
|
|
235
254
|
§1.1). Never a colour: a tier is a fact about review, not a warning. */}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
<Fact label="Trust">
|
|
256
|
+
<span className="flex flex-wrap items-baseline gap-1.5">
|
|
257
|
+
<Chip text={trust.tier} />
|
|
258
|
+
{trust.by === null ? null : (
|
|
259
|
+
<span className="font-normal text-fd-muted-foreground">
|
|
260
|
+
{trust.by}
|
|
261
|
+
{trust.at === null ? null : <> · {day(trust.at)}</>}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
</span>
|
|
265
|
+
</Fact>
|
|
266
|
+
{/* Actions ride tier ONE: a reader who wants the bytes wants them
|
|
267
|
+
immediately, and this is the row with room. */}
|
|
268
|
+
{markdownUrl === undefined && minutes === undefined ? null : (
|
|
269
|
+
<div className="ms-auto flex items-center gap-x-6">
|
|
270
|
+
{markdownUrl === undefined ? null : <DocumentActions href={markdownUrl} />}
|
|
271
|
+
{minutes === undefined ? null : (
|
|
272
|
+
<div className="flex items-center gap-2 text-sm text-fd-muted-foreground">
|
|
273
|
+
<Clock aria-hidden className="size-3.5 shrink-0" />
|
|
274
|
+
<span>{minutes} min read</span>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</dl>
|
|
280
|
+
{/* Tier two RECEDES. It is provenance — who let this in, when it takes
|
|
281
|
+
effect, what it replaced — and it is read when a reader goes looking,
|
|
282
|
+
not scanned. At full `--foreground` weight it competed with the two
|
|
283
|
+
chips above it for the same attention, which is what made a producer
|
|
284
|
+
id the loudest thing on the page.
|
|
285
|
+
|
|
286
|
+
Links keep full strength: `Replaces` points at the document this one
|
|
287
|
+
superseded, and that is an action rather than a fact. */}
|
|
288
|
+
<dl className="mt-2.5 flex flex-wrap items-baseline gap-x-8 gap-y-2.5 empty:mt-0 [&_a]:text-fd-foreground [&_dd]:font-normal [&_dd]:text-fd-muted-foreground">
|
|
289
|
+
{owner === null ? null : <Fact label="Owner">{owner}</Fact>}
|
|
290
|
+
{/* Who let this into the record. `ksor.approval` is what makes a `stable`
|
|
249
291
|
document stable at all (record spec §2.2), so a page that showed the
|
|
250
292
|
word and not the signature would be publishing the claim without its
|
|
251
293
|
author. */}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
294
|
+
{approval === null ? null : (
|
|
295
|
+
<Fact label="Approved">
|
|
296
|
+
<>
|
|
297
|
+
{approval.by} · {day(approval.at)}
|
|
298
|
+
</>
|
|
299
|
+
</Fact>
|
|
300
|
+
)}
|
|
301
|
+
{/* found live 2026-08-25: a deprecated page named its successor and said
|
|
260
302
|
nothing about WHO withdrew it, though `ksor.deprecated` is required on
|
|
261
303
|
every deprecated concept (record spec §2.2) and readGovernance already
|
|
262
304
|
refuses a document that omits it. Withdrawal is the most consequential
|
|
263
305
|
act in a document's life; publishing it unattributed is exactly the
|
|
264
306
|
gap the approver fact above closes at the other end. */}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// how a reader hands this document to an agent (research/site-design.md
|
|
304
|
-
// F2). Right-aligned: a column of things you DO, against a row of
|
|
305
|
-
// things the record DECLARES (owner, 2026-08-22).
|
|
306
|
-
<span className="ms-auto">
|
|
307
|
-
<DocumentActions href={markdownUrl} />
|
|
308
|
-
</span>
|
|
309
|
-
)}
|
|
310
|
-
{minutes === undefined ? null : (
|
|
311
|
-
<div className="ms-auto flex items-center gap-2 text-sm text-fd-muted-foreground">
|
|
312
|
-
<Clock aria-hidden className="size-3.5 shrink-0" />
|
|
313
|
-
<span>{minutes} min read</span>
|
|
314
|
-
</div>
|
|
315
|
-
)}
|
|
316
|
-
</dl>
|
|
307
|
+
{deprecated === null ? null : (
|
|
308
|
+
<Fact label="Withdrawn">
|
|
309
|
+
<>
|
|
310
|
+
{deprecated.by} · {day(deprecated.at)}
|
|
311
|
+
</>
|
|
312
|
+
</Fact>
|
|
313
|
+
)}
|
|
314
|
+
{replaces.length === 0 ? null : (
|
|
315
|
+
// The other half of a supersession. The withdrawn document names its
|
|
316
|
+
// successor above the title; this is the successor naming what it
|
|
317
|
+
// replaced, so the history the record kept is reachable from the
|
|
318
|
+
// current document instead of only from the retired one.
|
|
319
|
+
<Fact label="Replaces">
|
|
320
|
+
<>
|
|
321
|
+
{replaces.map((entry, index) => (
|
|
322
|
+
<span key={entry.href ?? `${index}-${entry.label}`}>
|
|
323
|
+
{index === 0 ? null : ", "}
|
|
324
|
+
{entry.href === null ? (
|
|
325
|
+
entry.label
|
|
326
|
+
) : (
|
|
327
|
+
<Link
|
|
328
|
+
href={entry.href}
|
|
329
|
+
className="underline underline-offset-4 transition-colors hover:text-fd-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fd-ring"
|
|
330
|
+
>
|
|
331
|
+
{entry.label}
|
|
332
|
+
</Link>
|
|
333
|
+
)}
|
|
334
|
+
</span>
|
|
335
|
+
))}
|
|
336
|
+
</>
|
|
337
|
+
</Fact>
|
|
338
|
+
)}
|
|
339
|
+
{effectiveFrom === null || !showEffective ? null : (
|
|
340
|
+
<Fact label="Effective from">{day(effectiveFrom)}</Fact>
|
|
341
|
+
)}
|
|
342
|
+
{staleAfter === null ? null : <Fact label="Review by">{day(staleAfter)}</Fact>}
|
|
343
|
+
</dl>
|
|
344
|
+
</div>
|
|
317
345
|
);
|
|
318
346
|
}
|
|
319
347
|
|
|
@@ -670,30 +670,51 @@ function fillStage(recordDir: string, stageDir: string, development: boolean): v
|
|
|
670
670
|
}
|
|
671
671
|
|
|
672
672
|
/**
|
|
673
|
-
* Dev only: carry edits
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* retitled
|
|
673
|
+
* Dev only: carry edits AND ARRIVALS into the stage, so `pnpm dev` shows the
|
|
674
|
+
* record as the owner is writing it rather than as it stood when the server
|
|
675
|
+
* started — the regenerated indexes included, so a retitled document is
|
|
676
|
+
* retitled in its folder's listing too.
|
|
677
677
|
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
678
|
+
* Adds and edits — never removals. The 2026-08-18 measurement this refused
|
|
679
|
+
* adds on ("fumadocs' own watcher cannot see a dot-prefixed collection
|
|
680
|
+
* directory") no longer holds: on fumadocs-mdx 15.3.0 a file written into
|
|
681
|
+
* `.staged-knowledge` DOES regenerate the collection, twice-observed as
|
|
682
|
+
* `[MDX] generated files` in the dev log. What actually kept a new document
|
|
683
|
+
* off every surface was this function, which walked the STAGE and skipped
|
|
684
|
+
* anything the stage did not already hold — so a plan entry with no file on
|
|
685
|
+
* disk was never written, and the manifest that names what publishes never
|
|
686
|
+
* learned about it either.
|
|
687
|
+
*
|
|
688
|
+
* Measured before and after, adding a document while `pnpm dev` ran:
|
|
689
|
+
* `/docs/<new>/` 404 -> 200, sidebar 0 -> 1, `llms.txt` 0 -> 1. It worked
|
|
690
|
+
* this way before the stage existed (0.0.40 serves an added document at 200),
|
|
691
|
+
* so this is a regression repaired rather than a feature.
|
|
692
|
+
*
|
|
693
|
+
* REMOVALS still wait for the restart `pnpm dev` already needs for
|
|
694
|
+
* instance.md: the same measurement found a deleted file leaves fumadocs'
|
|
695
|
+
* generated imports pointing at something gone, which takes the dev server
|
|
696
|
+
* down rather than showing a stale page. An arrival has no such failure mode
|
|
697
|
+
* — nothing points at a file that has only just appeared.
|
|
685
698
|
*/
|
|
686
699
|
function refreshStage(recordDir: string, stageDir: string): void {
|
|
687
700
|
// Under the lock like every other write here: a save landing while another
|
|
688
701
|
// evaluation is refilling the stage is the same race from the other side.
|
|
689
702
|
withStageLock(stageDir, () => {
|
|
690
703
|
const plan = planStage(recordDir, true);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
704
|
+
// Drive from the PLAN, not from the stage. Walking the stage could only
|
|
705
|
+
// ever find what was already there, which is exactly why an arrival was
|
|
706
|
+
// invisible: it has no file to walk onto.
|
|
707
|
+
for (const entry of plan.entries) {
|
|
708
|
+
const staged = path.join(stageDir, entry.rel);
|
|
695
709
|
const bytes = entry.bytes();
|
|
696
|
-
|
|
710
|
+
let current: Buffer | null = null;
|
|
711
|
+
try {
|
|
712
|
+
current = readFileSync(staged);
|
|
713
|
+
} catch {
|
|
714
|
+
// Not staged yet — an arrival. Written below.
|
|
715
|
+
}
|
|
716
|
+
if (current !== null && current.equals(bytes)) continue;
|
|
717
|
+
mkdirSync(path.dirname(staged), { recursive: true });
|
|
697
718
|
writeFileSync(staged, bytes);
|
|
698
719
|
}
|
|
699
720
|
writeManifest(stageDir, plan.manifest);
|