@panaversity/ksor 0.0.39 → 0.0.41
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 +965 -0
- package/README.md +29 -9
- package/dist/checker/check-main.mjs +14049 -0
- package/dist/cli.mjs +11528 -5213
- package/dist/gateway-api-CF4ED9_g-BQusM_dK.mjs +10895 -0
- package/dist/gateway.d.mts +52 -13
- package/dist/gateway.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-pl4aOpVs.mjs → src-dqpI-p1a.mjs} +1 -0
- package/docs/authorization.md +8 -6
- package/docs/deploying.md +36 -25
- package/docs/index.md +26 -13
- package/docs/ingesting.md +70 -22
- package/docs/tool-surface.md +69 -16
- package/package.json +4 -3
- package/schema/migrations/2.4-2.5__okf-profile.sql +114 -0
- package/schema/schema.sql +77 -14
- package/templates/scaffold/.agents/skills/add-sources/SKILL.md +63 -18
- package/templates/scaffold/.agents/skills/format-checker/SKILL.md +42 -33
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +13827 -1314
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +65 -27
- package/templates/scaffold/.agents/skills/make-slides/SKILL.md +7 -5
- package/templates/scaffold/.agents/skills/make-summary/SKILL.md +13 -6
- package/templates/scaffold/.claude/skills/add-sources/SKILL.md +63 -18
- package/templates/scaffold/.claude/skills/format-checker/SKILL.md +42 -33
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +13827 -1314
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +65 -27
- package/templates/scaffold/.claude/skills/make-slides/SKILL.md +7 -5
- package/templates/scaffold/.claude/skills/make-summary/SKILL.md +13 -6
- package/templates/scaffold/.github/workflows/validate.yml +9 -1
- package/templates/scaffold/.ksor/governance.yaml +17 -0
- package/templates/scaffold/AGENTS.md +293 -119
- package/templates/scaffold/Dockerfile +5 -1
- package/templates/scaffold/README.md +160 -42
- package/templates/scaffold/env.example +37 -6
- package/templates/scaffold/gitignore +14 -6
- package/templates/scaffold/instance.md +21 -17
- package/templates/scaffold/knowledge/governance-ladder.md +11 -2
- package/templates/scaffold/knowledge/index.md +9 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +13 -6
- package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
- package/templates/scaffold/knowledge/surfaces/index.md +4 -20
- package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
- package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
- package/templates/scaffold/package.json +3 -4
- package/templates/scaffold/pnpm-lock.yaml +3 -0
- package/templates/scaffold/system/gateways/content.ts +13 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
- package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +134 -91
- package/templates/scaffold/system/site/app/global.css +198 -6
- package/templates/scaffold/system/site/app/layout.tsx +8 -3
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
- package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
- package/templates/scaffold/system/site/components/code-block.tsx +87 -0
- package/templates/scaffold/system/site/components/embed.tsx +276 -0
- package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
- package/templates/scaffold/system/site/components/governance.tsx +219 -87
- package/templates/scaffold/system/site/components/mdx.tsx +10 -2
- package/templates/scaffold/system/site/components/record-index.tsx +5 -5
- package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
- package/templates/scaffold/system/site/components/record-views.tsx +6 -17
- package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
- package/templates/scaffold/system/site/lib/alert-rule.ts +214 -0
- package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
- package/templates/scaffold/system/site/lib/attachments.ts +0 -28
- package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
- package/templates/scaffold/system/site/lib/audience.ts +42 -146
- package/templates/scaffold/system/site/lib/embed-rule.ts +255 -0
- package/templates/scaffold/system/site/lib/governance.ts +339 -225
- package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
- package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
- package/templates/scaffold/system/site/lib/lock.ts +282 -0
- package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
- package/templates/scaffold/system/site/lib/record-href.ts +68 -0
- package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
- package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
- package/templates/scaffold/system/site/lib/shared.ts +67 -104
- package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
- package/templates/scaffold/system/site/lib/source.ts +256 -186
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +638 -467
- package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
- package/templates/scaffold/system/site/package.json +1 -0
- package/templates/scaffold/system/site/record/actor.ts +23 -0
- package/templates/scaffold/system/site/record/check.ts +571 -0
- package/templates/scaffold/system/site/record/citations.ts +312 -0
- package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
- package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
- package/templates/scaffold/system/site/record/hygiene.ts +320 -0
- package/templates/scaffold/system/site/record/index-file.ts +150 -0
- package/templates/scaffold/system/site/record/index.ts +103 -0
- package/templates/scaffold/system/site/record/instance.ts +257 -0
- package/templates/scaffold/system/site/record/instant.ts +43 -0
- package/templates/scaffold/system/site/record/ledger.ts +694 -0
- package/templates/scaffold/system/site/record/load.ts +129 -0
- package/templates/scaffold/system/site/record/lock.ts +306 -0
- package/templates/scaffold/system/site/record/near-miss.ts +37 -0
- package/templates/scaffold/system/site/record/policy.ts +414 -0
- package/templates/scaffold/system/site/record/profile.ts +535 -0
- package/templates/scaffold/system/site/record/refusal.ts +106 -0
- package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
- package/templates/scaffold/system/site/source.config.ts +109 -23
- package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
- package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
- package/templates/scaffold/system/site/lib/page-order.ts +0 -93
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,970 @@
|
|
|
1
1
|
# @panaversity/ksor
|
|
2
2
|
|
|
3
|
+
## 0.0.41
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fe9a0af: Refuse a withdrawal a document attests for itself, and close three record-checker gaps
|
|
8
|
+
|
|
9
|
+
- **`ksor-deprecator-unauthorised` no longer accepts `ksor.owner`.** The owner who
|
|
10
|
+
may withdraw a document is the one an `ownership:` rule in
|
|
11
|
+
`.ksor/governance.yaml` resolves — never the `ksor.owner` the document writes
|
|
12
|
+
about itself, which is free text the profile does not even form-check. Before
|
|
13
|
+
this, `ksor.owner: human:mallory` beside `ksor.deprecated.by: human:mallory`
|
|
14
|
+
passed in any record whose policy declares no `ownership:` rule, which is the
|
|
15
|
+
shape both `ksor init` and `ksor migrate` emit. That was asymmetric with
|
|
16
|
+
approval, where a policy naming no matching rule refuses outright.
|
|
17
|
+
|
|
18
|
+
**This is a behaviour change for existing records.** If your policy declares no
|
|
19
|
+
`ownership:` rule, a `deprecated` document must now name a
|
|
20
|
+
`takedown_authorities` actor in `ksor.deprecated.by`, or the build refuses.
|
|
21
|
+
Either record the withdrawal by a takedown authority, or add an `ownership:`
|
|
22
|
+
rule naming who owns that path.
|
|
23
|
+
|
|
24
|
+
- **A dot-prefixed or backslash-bearing filename under `knowledge/` is refused**
|
|
25
|
+
(`ksor-name-unportable`). `knowledge/.secret.md` used to pass with no refusal
|
|
26
|
+
at all and became a full concept: the MCP door served it while the site's file
|
|
27
|
+
walk, which does not match dot-prefixed names, had no route for it. A backslash
|
|
28
|
+
is a legal character in one Linux filename and the path separator on Windows,
|
|
29
|
+
where the checkout fails outright.
|
|
30
|
+
|
|
31
|
+
- **`index.summary.md` is refused (`ksor-attachment-of-index`).** A generated
|
|
32
|
+
index is not a document — no route, no node, no governance — so nothing can
|
|
33
|
+
attach to it, and the orphan rule could not see the problem because the
|
|
34
|
+
generated `index.md` IS committed. The file was accepted, stamped into
|
|
35
|
+
`build.lock.json`'s `companions[]` and into `build_id`, and then published on
|
|
36
|
+
no surface at all, silently. Decision 27 retires the `index.summary.md` row
|
|
37
|
+
from the canonical attachment table with it.
|
|
38
|
+
|
|
39
|
+
- **An `.mdx` summary is recognised as an attachment.** The checker kept its own
|
|
40
|
+
list of companion suffixes and it had drifted from the canonical one, so
|
|
41
|
+
`x.summary.mdx` got no orphan check, no `type: Summary` check and none of its
|
|
42
|
+
parent's governance. Both copies are now derived from the one list.
|
|
43
|
+
|
|
44
|
+
- fe9a0af: A cold start against a sleeping database no longer crash-loops the door, and an unauthenticated public bind says what it is actually handing out.
|
|
45
|
+
|
|
46
|
+
**`ksor serve` meant it when it said DEFERRED.** The door is built to come up when the content store is unreachable — it announces `boot checks DEFERRED … NOT READY`, refuses every request, and retries until the database answers, because a serverless Postgres waking from suspend is an ordinary deploy, not an exception. One read sat outside that guard: the viewer list is validated against the ingested policy's registry, which is a row, and reading it threw two statements after the DEFERRED line had already printed. The process exited 3, the platform restarted it, and it did the same thing again — a crash loop for a database that was merely asleep. That read is now one of the boot checks, so deferring defers it too, and until it passes the door holds the one viewer list that is legal for every record: `public`. Nothing is served through it, because an unverified instance refuses every request.
|
|
47
|
+
|
|
48
|
+
**A refusal is no longer deferred as though it were an outage.** A stemming mismatch between `instance.md` and the stored `search_tsv`, and a `KSOR_AUDIENCE` naming an audience the policy does not register, are both decided by a row the database ANSWERED with. They were caught by the deferral branch, which reported `content store unreachable` about a store that had just replied and left the door retrying a verdict no retry can change. Both now refuse at boot, where they can be fixed.
|
|
49
|
+
|
|
50
|
+
**`KSOR_AUTH=disabled-public` now states what it reaches.** The boot report carried two facts and never their product: one line said the door was unauthenticated, another said `audience public,internal`, and a door serving the internal half of the record to anonymous callers read exactly like one serving only the public half. The auth line now names the restricted tiers by name — and stops saying "the whole record" when only the public audience is being served, so the loud sentence means something when it is true.
|
|
51
|
+
|
|
52
|
+
- e23e07d: The door's refusals now say the right thing, to the right audience — and a governance refusal no longer names the documents it withdrew.
|
|
53
|
+
|
|
54
|
+
**A 503 stopped enumerating withdrawn documents.** Two governance refusals interpolated the `stable_id`s of documents somebody had TAKEN DOWN into the message a caller receives. Under `KSOR_AUTH=disabled-public` that reaches anyone who can reach the port: the strongest governance act in the product, listing what it had removed, to an unauthenticated agent. `GovernanceGateError` now takes those identifiers through a parameter of their own and appends them to the OPERATOR's copy alone — the one that goes to the server's logs, where the fix happens. The 503 keeps the slug, the count and the remedy, which is what a caller could act on anyway. The split is structural rather than a matter of care: there is no route from the identifier parameter to the text that goes on the wire.
|
|
55
|
+
|
|
56
|
+
**A refusal stopped misdiagnosing itself as a database outage.** Which errors are the door's own verdicts was written out in two places — the boot path that refuses them and the layer that decides what a caller may read — and the two lists disagreed. So a one-character typo in `KSOR_AUDIENCE`, on a container whose database happened to be asleep when it started, answered every request with `the content store is unavailable (AudienceError)`: a refusal blaming a database that had just replied, while the text naming the typo and the fix reached nobody. It is now one table, with each entry saying which half of its message is public and why, so the two cannot drift apart again. A record's registered audience names are treated like the withdrawn document paths above — the operator's logs get them, a caller does not.
|
|
57
|
+
|
|
58
|
+
**`/ready` stopped blaming the network for a refusal.** It collapsed every failure to `false` and answered `content store unreachable` — so a governance violation, about a database that had just replied and that no retry can fix, was reported forever as a network fault, while `POST /mcp` returned the real remedy. One door telling two stories. The probe now names the class of refusal and points at the logs. It deliberately says LESS than the 503 does: `/ready` is unauthenticated on every posture, including the bearer-gated one, so it is the wrong place to repeat a record's governance state.
|
|
59
|
+
|
|
60
|
+
**The embedding-space guard is retried instead of switched off.** It is fail-closed on a warm boot, but a cold start caught it, reduced it to a note on `/health`, and never ran it again — so a door that started against a sleeping database and recovered reported itself ready and then compared vectors across two embedding spaces, with the abstention floor calibrated in a space the record no longer used. It now sits with the other boot checks: deferred together, retried together, and a proven mismatch refuses.
|
|
61
|
+
|
|
62
|
+
**`/health` says when the boot checks have not passed**, instead of reporting a normal-looking posture while every request is being refused.
|
|
63
|
+
|
|
64
|
+
**The abstention line stopped claiming an armed gate on a door that refuses every search.** A `vector_floor` with no digest was measured against a retrieval predicate that has since changed, so the gate treats it as uncalibrated and refuses everything — and the boot report and `/health` both announced `floor 0.631 — below it, this record abstains`. The operator was told the record was abstaining at a measured threshold while it was in fact answering nothing at all, which is this product's own "honest absence, never silent weakness" rule inverted, and it is the first state an adopter meets after upgrading. Both surfaces now take the decision from one function, and say to run `ksor calibrate`.
|
|
65
|
+
|
|
66
|
+
**A Host or Origin allowlist is matched the way HTTP compares those values.** `KSOR_ALLOWED_HOSTS=MCP.Acme.com` used to reject every client that resolved it — a total outage from a valid setting, refused with a message that named neither the value nor the variable. Both sides are case-folded now, and both refusals carry their remedy, as does the 413 that used to say only that a body was too large.
|
|
67
|
+
|
|
68
|
+
- fa49d37: Make the emitted scaffold docs survive being followed literally
|
|
69
|
+
|
|
70
|
+
A first-hour walkthrough obeyed the emitted README word for word and hit four
|
|
71
|
+
dead-ends. Each is now fixed where the reader meets it, not only in the deep
|
|
72
|
+
doc that already got it right.
|
|
73
|
+
|
|
74
|
+
- **The publish recipe was incomplete and refused.** "`status: stable` with a
|
|
75
|
+
`ksor.approval`" is two thirds of it — `ksor-stable-ungenerated` also demands
|
|
76
|
+
`generated: { by, at }`, which appeared nowhere in the README. It now shows
|
|
77
|
+
the whole frontmatter shape, says which half is provenance (any producer) and
|
|
78
|
+
which is authority (an actor `.ksor/governance.yaml` lists), and names the
|
|
79
|
+
ordering rule between the two `at`s.
|
|
80
|
+
- **"The ordered path is:" was not the order.** The command block ran before
|
|
81
|
+
the instruction to uncomment `database:` in `instance.md`, which sat thirteen
|
|
82
|
+
lines below it, so step two died with `instance.md declares no database:
|
|
83
|
+
block`. The emitted `AGENTS.md` had the right order all along; the README now
|
|
84
|
+
matches it — config, environment, then commands.
|
|
85
|
+
- **The Docker smoke test refused with the `.env` the README told you to
|
|
86
|
+
write.** A container sets `$PORT`, so the door binds `0.0.0.0` and
|
|
87
|
+
`KSOR_AUTH=disabled-local` correctly refuses. The refusal is right and stays;
|
|
88
|
+
the printed command now carries `-e KSOR_AUTH=disabled-public` and says why,
|
|
89
|
+
on the command rather than in `.env` so an ordinary `ksor serve` keeps its
|
|
90
|
+
loopback posture. Fixed in the README, `AGENTS.md`, the `Dockerfile` header,
|
|
91
|
+
`.env.example` and `docs/deploying.md`, which now all print one recipe.
|
|
92
|
+
- **`ksor` reads `.env`, but a refusal says "export that variable".** Both are
|
|
93
|
+
true and a newcomer met both; the README now says so in one sentence.
|
|
94
|
+
|
|
95
|
+
Two smaller truths: `pnpm refresh` builds before it ingests, and both places
|
|
96
|
+
that describe it said otherwise; and `instance.md`'s own description of the
|
|
97
|
+
starter claimed "types, statuses, audiences, a folder and a companion summary"
|
|
98
|
+
where the starter is in fact five approved documents, all one type, one status
|
|
99
|
+
and one audience, three of them in a folder, with one carrying all four study
|
|
100
|
+
attachments.
|
|
101
|
+
|
|
102
|
+
New: a short **note on `audit`**. A fresh `npm install` ends with high-severity
|
|
103
|
+
advisories against the pinned `next` and an invitation to `npm audit fix
|
|
104
|
+
--force`, which would break the pin — and nothing said not to. The note says
|
|
105
|
+
don't let an audit tool raise the pin, explains the one structural reason the
|
|
106
|
+
report reads worse than it is (the site is a static export, so no framework
|
|
107
|
+
server, middleware, server actions, rewrites or image optimizer ever run), and
|
|
108
|
+
names what that argument does NOT cover: the build toolchain, and any served
|
|
109
|
+
route added later.
|
|
110
|
+
|
|
111
|
+
- 8a384be: Four places where the product was wrong about itself.
|
|
112
|
+
|
|
113
|
+
**A governance key one level from where the profile reads it is refused by
|
|
114
|
+
name** (`ksor-key-misplaced`). `effective_from:` at a concept's top level is
|
|
115
|
+
spelled correctly, so no near-miss net could see it and OKF §11 preserved it —
|
|
116
|
+
a document embargoed to 2099 built clean, exited 0 and published the same day.
|
|
117
|
+
The mirror, `ksor.stale_after`, was refused as a key of a closed block and told
|
|
118
|
+
the author to "remove `stale_after:`"; following that remedy on a document
|
|
119
|
+
already past that instant flipped it from withheld to published. Both
|
|
120
|
+
directions are now named, and no remedy in the profile deletes a governance
|
|
121
|
+
value — an unrecognised key under `ksor:` is moved to the top level, where §11
|
|
122
|
+
preserves it.
|
|
123
|
+
|
|
124
|
+
**Every surface now says whose claim the trust tier is.** `verified[].by` is
|
|
125
|
+
checked for its actor form and nothing else: the Governance Policy has no
|
|
126
|
+
verification family, so any well-formed `human:` actor promotes a document to
|
|
127
|
+
`human-reviewed` — while `ksor.approval.by` is refused outright when no rule
|
|
128
|
+
matches it. That asymmetry is deliberate (record spec §2.3) and unchanged; what
|
|
129
|
+
changed is that the `search` and `read` tool descriptions, the emitted
|
|
130
|
+
`.env.example` and the emitted `AGENTS.md` said or implied otherwise. At
|
|
131
|
+
`KSOR_MIN_TRUST_TIER=human-reviewed` the only document a record served was the
|
|
132
|
+
one asserting its own review. The tool definitions grow 520 chars for it
|
|
133
|
+
(16,214 → 16,734 as transmitted, ~4,054 → ~4,184 always-resident tokens);
|
|
134
|
+
`packages/ksor/docs/tool-surface.md` has the re-measured table.
|
|
135
|
+
|
|
136
|
+
**`ksor build` says what its own snapshot will stop being true.** Machine-surface
|
|
137
|
+
admission is decided once, at the build's `as_of`, and written into files that
|
|
138
|
+
cannot re-decide themselves — so a document whose `stale_after` passes after a
|
|
139
|
+
build keeps appearing in `llms.txt` and its markdown twin while `ksor serve`
|
|
140
|
+
already refuses it. The build now names the documents it held back and why, and
|
|
141
|
+
the next instant at which this goes out of date. It is a notice and not a
|
|
142
|
+
refusal: a document past its review date is a governed state, and a build that
|
|
143
|
+
refused it would make deleting the `stale_after` the fastest way to green. The
|
|
144
|
+
emitted `AGENTS.md` stated the exclusion unconditionally and now states the
|
|
145
|
+
rebuild obligation instead; the emitted `README.md` carries it too.
|
|
146
|
+
|
|
147
|
+
**Three `ksor takedown` remedies name `--actor`.** Decision 21 requires it on
|
|
148
|
+
every mode that writes the ledger, so the printed fix lines exited 1 on
|
|
149
|
+
`ksor-takedown-unattributed` when pasted.
|
|
150
|
+
|
|
151
|
+
- fe9a0af: Ingest now checks the whole lock, refuses an unaccountable takedown before it spends anything, and never serves a tier nobody asked for
|
|
152
|
+
|
|
153
|
+
**`ksor ingest` reads every digest `ksor build` records, not just the document
|
|
154
|
+
hashes.** The lock covers the instance, the governance policy, the takedown
|
|
155
|
+
ledger, the companions, the assets and the generated indexes — and ingest was
|
|
156
|
+
comparing only `documents[]`. So a governance file edited _after_ the build that
|
|
157
|
+
checked it went straight into a published generation: delete a denial's four
|
|
158
|
+
lines from `.ksor/takedowns.yaml`, ingest, and the MCP door published a document
|
|
159
|
+
the website still withdrew. Editing any of them without rebuilding now refuses
|
|
160
|
+
`ksor-lock-stale` and names the file. Re-run `ksor build`, commit both, ingest.
|
|
161
|
+
|
|
162
|
+
**A denial nothing in the repository accounts for now stops ingest where it
|
|
163
|
+
happens.** A record upgraded from schema 2.4 carries denylist rows with no
|
|
164
|
+
ledger entry, and `ksor serve` refuses to boot on exactly that. Ingest used to
|
|
165
|
+
say nothing, build and embed a whole generation, and only then refuse — leaving
|
|
166
|
+
an un-activated generation behind. It now refuses at the ledger step, before a
|
|
167
|
+
generation is allocated, with the same `ksor-takedown-unledgered` slug and the
|
|
168
|
+
remedy that resolves it (`ksor migrate --write`, commit, ingest).
|
|
169
|
+
|
|
170
|
+
**A read that names no audience is served nothing.** The kernel's read path bound
|
|
171
|
+
"the whole record" as its default viewer, which meant the SQL rule that denies an
|
|
172
|
+
unstated viewer could never fire. The default is gone: callers entitled to the
|
|
173
|
+
whole record say so, and everything else fails closed. An audience identifier
|
|
174
|
+
containing the list separator, or spelled `*`, is refused
|
|
175
|
+
(`ksor-audience-identifier-invalid`) rather than silently read as a different set
|
|
176
|
+
of audiences.
|
|
177
|
+
|
|
178
|
+
**A withdrawn-then-deleted document no longer bricks the record.** Deleting a
|
|
179
|
+
document after withdrawing it is the sequence the record spec sanctions, and
|
|
180
|
+
`ksor migrate --write` produces it on its own for any denial whose document is
|
|
181
|
+
already gone. The denylist row carried no record of that, so the boot check read
|
|
182
|
+
"no document with this id" as an orphaned denial and refused `ksor ingest` and
|
|
183
|
+
`ksor serve` permanently — while `ksor build` and the website stayed green. The
|
|
184
|
+
remedy it printed could not clear it: `ksor takedown --removed` records what
|
|
185
|
+
happened to the FILE and moves no row, so the only escape was to un-withdraw the
|
|
186
|
+
document. The row now carries `expected`, and a document the record itself
|
|
187
|
+
documents as removed is no longer read as an orphan. It stays withdrawn: the
|
|
188
|
+
serving predicate never reads that column.
|
|
189
|
+
|
|
190
|
+
**A migrated database is now the same database as a fresh one.** Nothing compared
|
|
191
|
+
the two; a schema-parity check across columns, constraints, indexes, policies,
|
|
192
|
+
privileges and triggers found the profile's two CHECK constraints carrying
|
|
193
|
+
different names on each side, and `schema.sql` now names them as the migration
|
|
194
|
+
does.
|
|
195
|
+
|
|
196
|
+
Also: an empty `sources:` list is the same value as no `sources:` list
|
|
197
|
+
everywhere, so it no longer changes a generation's provenance digest depending
|
|
198
|
+
on which side of a round trip it is read from.
|
|
199
|
+
|
|
200
|
+
**A malformed argument no longer reports itself as an outage.** A value Postgres
|
|
201
|
+
cannot represent — a NUL byte in a slug or a query is the reachable case — made
|
|
202
|
+
every read fail with "content store temporarily unavailable". The condition is
|
|
203
|
+
deterministic and harmless to the connection, but the tool guidance this door
|
|
204
|
+
hands every agent says `unavailable` means retry later and never conclude the
|
|
205
|
+
thing is absent, so a caller with one bad argument was told to retry forever
|
|
206
|
+
while the store answered everyone else. SQLSTATE class 22 is now reported as
|
|
207
|
+
what it is: the request was rejected as written, the store is healthy, and
|
|
208
|
+
retrying it unchanged will not help. Connection failures are unchanged.
|
|
209
|
+
|
|
210
|
+
- 0a0b048: Three governance rules that judged the wrong thing.
|
|
211
|
+
|
|
212
|
+
**A takedown ledger no longer breaks on a personnel change.** Every entry was checked against the takedown authorities named in `.ksor/governance.yaml` _today_, so removing someone who had left the organisation refused every entry they had ever written — the record stopped building for a personnel change, not a governance one — and the obvious remedy, deleting those entries, is `ksor-ledger-shrank`. The only escape was to go on naming a departed person as an authority, which is a lie the policy would then carry. Authority is now checked where the act happens: an entry this record has not yet accepted. The committed `build.lock.json` is the only evidence of acceptance, because it is written by a build that got past this very check; git history proves only that a line was committed, which anyone with write access can do, so a hand-appended entry in a pull request is still refused exactly as before. Acceptance is of TEXT, so an entry retargeted under an accepted id is judged again.
|
|
213
|
+
|
|
214
|
+
**A Governance Policy path that can never match is refused instead of silently ignored.** Scope paths are bundle-relative directory prefixes, and a concept's id carries neither its `.md` nor the `knowledge/` prefix — so `paths: ["hr/handbook.md"]` and `paths: ["knowledge/hr/"]`, the two forms a hand reaches for first, matched nothing at all. The tightly scoped rule simply never applied and approval or ownership resolution fell through to whatever broader rule was left, with nothing red on any surface. Both are now `ksor-policy-invalid`, naming the path and the one it would have to be. A bare `/` still means the whole record.
|
|
215
|
+
|
|
216
|
+
**A legal hold over the whole record is refused instead of half-performed.** A ledger entry naming the record root (`stable_id: knowledge/#section`, `scope: subtree`) was accepted, and only one of the two surfaces could carry it out. The site reads the empty prefix as everything, so the website went dark; the serving side walks `parent_id` from the node the denylist row names, and no node exists for the record root — top-level sections have no parent — so its seed was empty and the MCP door went on serving every document. Measured on a live 187-document record: the website dark, the door answering in full. The website going dark then reads as confirmation that a hold is in place over a door that never stopped. It is now `ksor-takedown-dangling`, naming the form that works — one subtree entry per top-level section — and it is raised on the in-force set rather than at parse time, so the entry stays readable and `ksor takedown --revoke` remains the way out.
|
|
217
|
+
|
|
218
|
+
- fe9a0af: **The link checker no longer goes blind on two ordinary markdown shapes.** Every link rule — `ksor-link-widens`, `ksor-link-dead`, `ksor-link-escapes` — and the footnote rule read the document with its code stripped out, so a shape the stripper mistook for code took its links out of reach of all four at once, with nothing red. A public document could point at a restricted one and `ksor build` exit 0.
|
|
219
|
+
|
|
220
|
+
**A list item's continuation paragraph is prose, not code.** CommonMark requires four spaces of indent there, and the stripper read any four-space line as an indented code block — so the link in
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
- A bullet.
|
|
224
|
+
|
|
225
|
+
See [the plan](/secret/plan.md) for detail.
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
was invisible to the checker while the site published it as a live link. Indentation is now measured from the container's content column, the way CommonMark measures it: code inside an item starts four columns past the ITEM, and a fenced sample inside an item is still a fence. A sub-bullet at the same indent was always seen, which is why this hid.
|
|
229
|
+
|
|
230
|
+
**A fence that never closes now hides only itself.** The fence state survived to end of input, so one stray ` ``` ` in prose silenced every link and footnote after it — half a document unjudged, with no signal. An unclosed fence is a stray backtick run, not a block, and the rest of the document is judged again.
|
|
231
|
+
|
|
232
|
+
Both directions were checked: no document in the shipped starter or the example corpus changes shape under the new stripper, and everything that was really code — indented blocks, fenced blocks inside list items, code spans, thematic breaks — is still stripped.
|
|
233
|
+
|
|
234
|
+
- 4d67703: Make six CLI messages answer the question the reader is actually holding
|
|
235
|
+
|
|
236
|
+
Every one of these came out of a first-hour walkthrough that followed the
|
|
237
|
+
printed output literally.
|
|
238
|
+
|
|
239
|
+
- **`ksor calibrate`'s paste block is now a block you can paste.** It ended with
|
|
240
|
+
"Paste into instance.md:" and then `vector_floor:` / `floor_digest:` at the
|
|
241
|
+
top level — where neither key lives. Pasted verbatim, the file was refused
|
|
242
|
+
(`ksor-instance-format`), so nothing shipped, but the one instruction the
|
|
243
|
+
report gives was wrong. It now prints the `retrieval:` block those keys belong
|
|
244
|
+
in, at column 0, because two spaces of terminal indentation lands inside a
|
|
245
|
+
frontmatter as a nested mapping and `yaml` refuses it outright. The
|
|
246
|
+
non-separable verdict's fail-closed block moved to the end of the report for
|
|
247
|
+
the same reason, and both are asserted by pasting them into a real instance.md
|
|
248
|
+
and parsing it.
|
|
249
|
+
|
|
250
|
+
- **A misplaced instance key now NAMES the block it belongs to.** The refusal
|
|
251
|
+
said "nest it under the block it belongs to" without ever saying which block
|
|
252
|
+
— and the file already holds the map that answers it. `vector_floor` and
|
|
253
|
+
`floor_digest` are told they are keys of `retrieval:`, and the remedy prints
|
|
254
|
+
the block with the values the file already carries, so the fix moves the
|
|
255
|
+
setting rather than dropping it.
|
|
256
|
+
|
|
257
|
+
- **A port already held now names its remedy.** `ksor serve` printed its boot
|
|
258
|
+
lines and then a bare Node errno — `error: listen EADDRINUSE` and the address,
|
|
259
|
+
with no `fix:` and no mention of `KSOR_MCP_PORT`. It now says what is wrong,
|
|
260
|
+
why, how to find the process holding the port, and how to serve on another
|
|
261
|
+
one; `EACCES` and `EADDRNOTAVAIL` get their own remedies, because the next
|
|
262
|
+
command differs. The exit code is unchanged: a bind failure
|
|
263
|
+
is the environment (3), never a refusal.
|
|
264
|
+
|
|
265
|
+
- **`ksor serve --help` and `ksor init --help` have pages.** Both fell through to
|
|
266
|
+
the generic verb list while every other verb answered for itself. `serve`'s
|
|
267
|
+
page names its one flag and the environment variables a first run needs —
|
|
268
|
+
including the one a busy port sends you looking for.
|
|
269
|
+
|
|
270
|
+
- **Every write-plane refusal opens with `error: <slug>`.** `ksor build` printed
|
|
271
|
+
a machine-readable slug alone on the first stderr line and `ksor schema` printed
|
|
272
|
+
none at all, for the same malformed file — so an agent reading `stderr` got a
|
|
273
|
+
different shape per verb. `schema`, `ingest`, `calibrate`, `grant`, `takedown`
|
|
274
|
+
and `gc` now keep the contract the docs already stated, naming the RECORD's own
|
|
275
|
+
slug where a record file is what refused. A bad `--dim` is `bad-args` rather
|
|
276
|
+
than a slug about an instance it never read.
|
|
277
|
+
|
|
278
|
+
- **A refusal states its reason once.** `ksor serve`, `ingest`, `schema`,
|
|
279
|
+
`calibrate`, `gc` and `grant` printed the same sentence twice — inline on the
|
|
280
|
+
`error:` line and again under `why:`.
|
|
281
|
+
|
|
282
|
+
- **`ksor build` says what it could not record about provenance.** On a record
|
|
283
|
+
with no commit it said only `(dirty)` — a word no shipped document defines —
|
|
284
|
+
and wrote `"source_commit": null` in silence, while `ksor ingest` explained the
|
|
285
|
+
identical state in full. Build now prints the same sentences ingest does, from
|
|
286
|
+
one shared module: the commit it published from when there is one, and what is
|
|
287
|
+
missing and how to fix it when there is not. It still does not refuse — a
|
|
288
|
+
provenance-less build is legitimate, and `--strict` is there for anyone who
|
|
289
|
+
wants it refused.
|
|
290
|
+
|
|
291
|
+
- 8a384be: **`ksor migrate` no longer widens an audience, brick a record, or emit a tree
|
|
292
|
+
it then refuses.** Three defects found by walking the upgrade path end to end
|
|
293
|
+
against a real database and a real door.
|
|
294
|
+
|
|
295
|
+
- **A re-run republished an internal record as public.** Migrate writes
|
|
296
|
+
`instance.md` first and deletes the `audiences:` model from it, so a record
|
|
297
|
+
that reached a second run with pre-profile documents still on it had nothing
|
|
298
|
+
to expand them against — and the fallback for "no model" is `[public]`. The
|
|
299
|
+
route needed no crash: `ksor migrate --write`, `git restore knowledge/`,
|
|
300
|
+
`ksor migrate --write --approve-by human:x` left a `default_visibility:
|
|
301
|
+
internal` document readable by every unauthenticated caller, at exit 0 with a
|
|
302
|
+
clean diff. Losing the model now REFUSES, naming the document and both ways
|
|
303
|
+
out; and `instance.md` is written LAST, so an interrupted run leaves the
|
|
304
|
+
model readable and is simply re-runnable.
|
|
305
|
+
- **A record that had ever withdrawn a section could not be published after
|
|
306
|
+
upgrading.** A denial anchored on `<dir>/index.md` follows the prose migrate
|
|
307
|
+
moves to `<dir>/overview.md`, and the `takedown_denylist` row it came from
|
|
308
|
+
still named the old path — accounted for by nothing, so `ksor ingest` refused
|
|
309
|
+
`ksor-takedown-unledgered` and `ksor serve` refused to boot. The remedy they
|
|
310
|
+
both print, `ksor migrate --write`, answered "nothing to migrate": the
|
|
311
|
+
transcription ran only into a record with no ledger at all. The stock
|
|
312
|
+
scaffold ships `knowledge/surfaces/index.md`, so the trigger was in every
|
|
313
|
+
adopter's tree. Migrate now records the row as it stands alongside the
|
|
314
|
+
repointed hold, and APPENDS to an existing ledger any row nothing accounts
|
|
315
|
+
for — which rescues a record already in that state. An existing entry is
|
|
316
|
+
still never rewritten, and an existing ledger is never regenerated.
|
|
317
|
+
- **Migrate wrote a supersession pointer `ksor build` refuses.** A
|
|
318
|
+
`superseded_by:` resolving to no concept — commonly a bare name resolved
|
|
319
|
+
against the document's own folder — was written out, followed by "Run
|
|
320
|
+
`ksor build`", which refused it as `ksor-supersession-strands`. It is now
|
|
321
|
+
refused up front, naming what was written, what it resolved to, and the
|
|
322
|
+
concept that is actually there under that name.
|
|
323
|
+
|
|
324
|
+
- 9e50b4b: `ksor build` runs (`specs/ksor/build/spec.md`): it generates every `knowledge/**/index.md` in memory, runs the record checker, and on green writes the indexes whose bytes changed plus `build.lock.json` — `build_id` over everything a projection reads, `source_commit` from the last commit touching an input, `dirty`, `as_of` (`--as-of` pins it), the canonical viewers and each document's admitted set. A refusal exits 1 with the slug first and writes nothing; `--strict` refuses an uncommitted input; the takedown ledger is checked for deleted entries against every version git has seen and the committed lock, and a shallow clone is refused unless `--allow-unverifiable-ledger`. The scaffold's `check.mjs` is now GENERATED from the record module at package-build time — one rule set with `ksor build`, bundled with its YAML parser (ISC notice in the banner), read-only, refusing a stale index — and the hygiene rules the hand-written checker carried (portable names, file types, PNG integrity, dead and escaping links, the instance's closed key set) live in the record module with stable slugs. The starter is rewritten in the KSoR Profile: `instance.md` format 2, `.ksor/governance.yaml`, `type: Document` concepts, generated indexes; every manager's `build` script is `ksor build` followed by the site build, and `export-denylist` is removed. A `ksor.superseded_by` pointer on a concept that is not `deprecated` is refused (`ksor-supersession-strands`), as the hand-written checker did.
|
|
325
|
+
- b3f1db6: The MCP door now says what the record knows about a passage, and lets a caller ask for better.
|
|
326
|
+
|
|
327
|
+
`search` accepts `min_trust_tier` — `unverified`, `machine-confirmed` or `human-reviewed` — so an agent can ask to be answered only from documents someone has reviewed. `KSOR_MIN_TRUST_TIER` sets the deployment's own floor, and the two compose by one rule: the higher of the pair. Configuration TIGHTENS and a request never loosens, so a door configured for `human-reviewed` cannot be talked down by an argument. The floor is bound into the retrieval arms, never applied to the hits afterwards — a floor enforced after ranking has already let a lower-tier passage decide what the answer was.
|
|
328
|
+
|
|
329
|
+
The default and the enforcement live in the handler, not in `system/gateways/content.ts`. A registration scaffolded before this release keeps working exactly as it did; the door NOTICES the missing parameter at boot, names the tool by the name you gave it and prints the line to paste, and then opens. Nothing is weakened by its absence — only the capability is gone.
|
|
330
|
+
|
|
331
|
+
**Every hit now carries `governance`**: the document's `status`, its `trust_tier`, the latest `verified` act (or null when nobody has reviewed it — an honest state of a governed record, not a defect), `effective_from`, `stale_after`, and `approval` with `checked: "policy"`. That last word is deliberate and is the whole point of the key: the approver was checked against your Governance Policy's authority list and NOT against change control, which lands in phase B. An envelope that said only "approval" would let an agent report more verification than happened.
|
|
332
|
+
|
|
333
|
+
**`read` carries the same `governance` block**, from the same stored columns, taken from the record's live row rather than a pinned one — a snapshot keeps a citation resolving to the same bytes, it does not freeze what the record has since decided about them. It sits beside the frontmatter on purpose: the frontmatter is what the author DECLARED and is untrusted corpus text, `governance` is what the record checked and stored, and the tool description says which is which.
|
|
334
|
+
|
|
335
|
+
**`read` returns the concept's frontmatter**, byte-exact as its author wrote it — comments and keys ksor has never heard of included. Not a re-serialisation: the profile preserves unknown keys, so a re-rendered block would be a different document wearing the record's name. Schema 2.5 gains `sources.frontmatter` for it, additively; existing records pick it up at their next `ksor ingest`.
|
|
336
|
+
|
|
337
|
+
**Every serving act's audit row records its scope** — the viewer list, the trust floor that applied, whether it abstained, how many results came back, and the generation. Never the passages and never the query: a trail that accumulated content would be a second copy of your record with no audience predicate over it and no takedown seam bound to it.
|
|
338
|
+
|
|
339
|
+
The frontmatter is a second untrusted channel, so the in-band injection advisory now reads BOTH: a `paste this into your agent` line in a frontmatter value raises `content_advisory` exactly as the same line in the prose does. It did not before, and a programmatic consumer re-reads the payload each turn and never the tool description.
|
|
340
|
+
|
|
341
|
+
A `min_trust_tier` your record cannot recognise is now REFUSED (`ksor-trust-floor-unknown`) instead of being read as "no floor". It used to resolve to -1 and serve the whole record — the opposite of what the same rule does for `KSOR_MIN_TRUST_TIER`, which has always refused a value it does not know rather than falling back.
|
|
342
|
+
|
|
343
|
+
Costs, recorded rather than argued away: the served `tools` array is now 16,734 characters — ~4,184 always-resident tokens, against the ~2,990 decision 23 recorded, with `search` growing 5,383 → 7,932 and `read` 3,396 → 5,466. The three definitions' own JSON sums to 16,730 of that; the array adds the brackets and the separators. Each `search` hit — and each `read` reply — carries 262 characters more where the document has a verification and an approval, 133 where a level-0 record has neither. `packages/ksor/docs/tool-surface.md` has the re-measured table and says which of its numbers are exact and which are derived.
|
|
344
|
+
|
|
345
|
+
- d1bc2a2: Wire the kernel onto the record module: `ksor ingest` reads every document through the ONE profile reader instead of its own frontmatter scanner, and schema 2.5 stores what it finds. `content_nodes.visibility` becomes `audience TEXT[]` (GIN-indexed, ranked tiers mapped to a one-element list and then dropped); the authored status is mapped and CHECKed onto `draft | stable | deprecated`; `sources`, `verified`, `generated`, `approval` and `deprecated` land as JSONB beside `effective_from`, `stale_after` and a derived `trust_tier`; each ingestion run records the `build_id` it published, the Governance Policy as a row with its digest, and the takedown ledger's id set; and `takedown_denylist` gains the ledger entry that wrote each row and the one that revoked it. Existing databases walk `2.4 -> 2.5` through `schema/migrations/`. A generation built before 2.5 refuses to serve until it is re-ingested, because the migration can only narrow a ranked tier and answering a viewer from a half-mapped row is not a thing a system of record may do.
|
|
346
|
+
|
|
347
|
+
`ksor ingest` now refuses the whole record before it writes anything: the record checker, then `build.lock.json` (`ksor-lock-missing` / `ksor-lock-stale` when the tree has moved since the build), then the ledger — and it applies ledger entries in file order, so a takedown reaches the database even when no document byte changed. `instance.md` is `format: 2`: `audiences:` and `default_visibility:` move to `.ksor/governance.yaml` and are refused with the migration hint, `title`, `description` and `toolchain` arrive. Two live defects go with it — a `CHUNK_POLICY` bump no longer reports "unchanged" against a generation chunked under the old policy, and a revoked denial no longer keeps denying.
|
|
348
|
+
|
|
349
|
+
`ksor takedown` is rewritten ledger-first (`specs/ksor/record/spec.md` §5): the act is appended to the committed, append-only `.ksor/takedowns.yaml` and only then written as the denylist row, so a record with no database gets takedown for the first time and the site reads denials from the repository. `--revoke <entry id>` lifts a denial by adding an entry rather than deleting a line, `--removed` records that a denied document was deleted, `--file-only` writes the entry alone, and `--apply` writes every unapplied entry's row under its own recorded actor. The verb refuses an actor `takedown_authorities` does not name before any DSN is resolved. **Breaking:** `ksor takedown --export`, `.ksor-denylist.json` and the scaffold's `export-denylist` step are removed.
|
|
350
|
+
|
|
351
|
+
- 38ac704: `ksor migrate` rewrites a record written before the KSoR Profile into it, and
|
|
352
|
+
prints a unified diff before it writes anything. `visibility` expands UPWARD
|
|
353
|
+
through the old ordered audience model (`internal` under
|
|
354
|
+
`[public, internal, board]` becomes `[internal, board]` — a one-element list
|
|
355
|
+
would silently drop the document from the board build); `provenance` strings
|
|
356
|
+
become `sources` with the string as the scope descriptor; `effective` widens to
|
|
357
|
+
midnight UTC; `review` becomes `draft` and `superseded` becomes `deprecated`
|
|
358
|
+
with an attributed `ksor.deprecated` and a `ksor.superseded_by` resolved to a
|
|
359
|
+
concept id; `approved` becomes `draft` unless `--approve-by` names the human
|
|
360
|
+
performing the approval; the instance becomes format 2 with its authority moved
|
|
361
|
+
into a written `.ksor/governance.yaml`; a reserved `index.md`/`README.md`
|
|
362
|
+
carrying prose moves to `overview.md`; every summary companion gains
|
|
363
|
+
`type: Summary`; and every denylist row in the database becomes a committed
|
|
364
|
+
ledger entry. It never authors knowledge: a title, a description, a
|
|
365
|
+
`generated.at` or a takedown actor it cannot derive is refused by name
|
|
366
|
+
(`ksor-migrate-underivable`). `--write-site` offers the site's byte-copied rule
|
|
367
|
+
modules in the same diff. The adopter's frontmatter comments survive — the
|
|
368
|
+
commented-out `database:` block in `instance.md` is their runbook.
|
|
369
|
+
|
|
370
|
+
A top-level `superseded_by:` is now refused as a pre-profile key rather than
|
|
371
|
+
preserved as an unknown one: the profile reads `ksor.superseded_by`, so a
|
|
372
|
+
top-level one announced a successor no surface showed.
|
|
373
|
+
|
|
374
|
+
The scaffold's skills are rewritten for the profile — the intake interview
|
|
375
|
+
gains a seventh question (who may approve, who may withdraw) and writes
|
|
376
|
+
`.ksor/governance.yaml`; `add-sources` emits profile frontmatter with `sources`
|
|
377
|
+
and footnote citations and never records an approval; `make-summary` emits
|
|
378
|
+
`type: Summary`. `.env.example` documents `KSOR_AUDIENCE` as the comma list of
|
|
379
|
+
audiences it is, always including `public`.
|
|
380
|
+
|
|
381
|
+
- 466d573: **The record is an OKF bundle now.** `knowledge/` is Markdown in the KSoR
|
|
382
|
+
Profile of the Open Knowledge Format — the shape the README has described for
|
|
383
|
+
weeks and the code did not hold. One rule set reads it: the CLI, the site and
|
|
384
|
+
the MCP door all check the same frontmatter through the same module, so a
|
|
385
|
+
document that publishes on one surface can no longer be refused on another.
|
|
386
|
+
|
|
387
|
+
It stays a `patch` — the record format is still finding its shape and the
|
|
388
|
+
0.0.x band says so — but it changes surfaces adopters depend on, so read the
|
|
389
|
+
upgrade path below before taking it. In one place, what moves:
|
|
390
|
+
|
|
391
|
+
- **A document's frontmatter.** `type`, `title`, `description`, `status`
|
|
392
|
+
(`draft | stable | deprecated`) and `ksor.audience` (a list) are required;
|
|
393
|
+
`stable` additionally carries `generated` and an `ksor.approval` by an actor
|
|
394
|
+
the Governance Policy authorises. `visibility`, `owner`, `provenance`,
|
|
395
|
+
`effective`, `superseded` and `sor_id` are refused **by name**, with the
|
|
396
|
+
migration named in the refusal.
|
|
397
|
+
- **Two files beside the bundle.** `.ksor/governance.yaml` says who may
|
|
398
|
+
approve and who may take down; `.ksor/takedowns.yaml` is the committed,
|
|
399
|
+
append-only takedown ledger, and it appears the first time something is
|
|
400
|
+
withdrawn — no file is the honest way to say nothing ever has been. Both
|
|
401
|
+
are tracked, not scratch: `.gitignore` ignores `.ksor/*` and un-ignores
|
|
402
|
+
these two by name.
|
|
403
|
+
- **`instance.md` is `format: 2`** — `audiences:` and `default_visibility:`
|
|
404
|
+
move into the policy; `title`, `description` and `toolchain:` arrive.
|
|
405
|
+
- **Two new verbs.** `ksor build` (database-free: generate the indexes, check
|
|
406
|
+
the record, write `build.lock.json`) and `ksor migrate` (rewrite a
|
|
407
|
+
pre-profile record, printing a diff before it writes anything).
|
|
408
|
+
- **Removed:** `ksor takedown --export`, `.ksor-denylist.json` and the
|
|
409
|
+
scaffold's `export-denylist` step. The site reads denials from the ledger,
|
|
410
|
+
so a record with no database has takedown for the first time.
|
|
411
|
+
|
|
412
|
+
**To upgrade an existing record**, in order:
|
|
413
|
+
|
|
414
|
+
```sh
|
|
415
|
+
pnpm add -D @panaversity/ksor@latest
|
|
416
|
+
ksor migrate --actor human:<you> # prints the diff, writes nothing
|
|
417
|
+
ksor migrate --write --actor human:<you> --approve-by human:<you>
|
|
418
|
+
ksor build
|
|
419
|
+
# a served record, after committing the migration:
|
|
420
|
+
ksor schema --instance instance.md --apply # 2.4 -> 2.5
|
|
421
|
+
ksor ingest --instance instance.md --flip
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
`ksor migrate` never authors knowledge: a document whose `description` it
|
|
425
|
+
cannot derive is refused by name rather than filled in, and an `approved`
|
|
426
|
+
document becomes a `draft` unless `--approve-by` names the human doing the
|
|
427
|
+
approving. Both are the same rule — a governance act names the actor who
|
|
428
|
+
performed it, and the tool does not guess one.
|
|
429
|
+
|
|
430
|
+
**That is why `--approve-by` is in the block above, and what happens without
|
|
431
|
+
it.** Every `approved` document becomes a `draft`, and a draft reaches no
|
|
432
|
+
machine surface at all: the next `ksor build` reports `0 admitted to a machine
|
|
433
|
+
surface`, and `llms.txt`, the `/md/` twins and the MCP door publish nothing
|
|
434
|
+
until a human approves. Where one document supersedes another it does not even
|
|
435
|
+
get that far — `ksor build` refuses with `ksor-supersession-strands`, because
|
|
436
|
+
the successor migrate just demoted is a draft and a reader sent to it would be
|
|
437
|
+
stranded. Pass `--approve-by human:<you>` when you are the person
|
|
438
|
+
`.ksor/governance.yaml` authorises to approve; otherwise expect to approve the
|
|
439
|
+
record document by document before it publishes again.
|
|
440
|
+
|
|
441
|
+
**Two things will refuse until you act, deliberately.** A generation ingested
|
|
442
|
+
before schema 2.5 will not serve until it is re-ingested, because the
|
|
443
|
+
migration can only narrow a ranked tier and half a governance row is not
|
|
444
|
+
something a system of record answers from. And a calibrated
|
|
445
|
+
`retrieval.vector_floor` measured before this release carries no
|
|
446
|
+
`floor_digest`, so the door REFUSES every `search`, `read` and `outline`
|
|
447
|
+
(`ksor-uncalibrated`; the search envelope's `gate` reads `"uncalibrated"`
|
|
448
|
+
rather than `"off"`) until `ksor calibrate` re-measures it through the
|
|
449
|
+
predicate that now applies. It refuses rather than abstains, deliberately: an
|
|
450
|
+
abstention would tell the caller this record does not cover the question, when
|
|
451
|
+
what is true is that the gate cannot be trusted to decide. A threshold carried
|
|
452
|
+
across a predicate change stays plausible and stops meaning what it said.
|
|
453
|
+
|
|
454
|
+
- b14a82c: Land the record module (`packages/content/src/record/`) that the OKF-native record (`specs/ksor/record/spec.md`) will be checked and built by: a frontmatter splitter that reads real YAML and refuses anything else as `ksor-frontmatter-invalid`; the concept profile as a zod schema with one refusal slug per rule; the Governance Policy reader with KSP 4.2.5 scope resolution; the takedown ledger reader (unauthorised actor, dangling, re-added, shrank); the OKF §8 index generator; footnote and link reading in both OKF link forms; the overlap, widening and lifecycle rules with their decision tables; and `checkRecord`, one rule set over an in-memory tree. The CLI now carries `yaml` (2.9.0, ISC, zero transitive dependencies; decision 26) as a runtime dependency, because a profile-shaped document's `ksor:` block and the `.ksor/*.yaml` control files are real YAML that no line scanner can read. Everything else in this release reads the record through this one module — `ksor build`, `ksor ingest`, the MCP door, the site's staging and the emitted `pnpm check` — which is what makes one rule set one rule set.
|
|
455
|
+
- d4061a5: A second review pass over the OKF-native record, and two of its findings were holes in the fixes themselves.
|
|
456
|
+
|
|
457
|
+
**An asset is judged by every directory above it, not only the one it sits in.** A public document linking `/secret/chart.svg` was refused; the same document linking `/secret/img/chart.svg` was not, because `secret/img/` holds no concept of its own and the rule read that as a shared `images/` folder. `ksor build` exited 0 and the public site carried the restricted directory's name and the asset's bytes. The check now climbs to the nearest ancestor that holds a concept, which leaves genuine shared folders alone and closes the nesting.
|
|
458
|
+
|
|
459
|
+
**A `.DS_Store` no longer makes the site unbuildable.** The stage walked `knowledge/` itself to decide what an asset is, while `build.lock.json` was written from the record loader — which skips OS junk and never reads a symlink as bytes. So the first time Finder touched `knowledge/`, every local `pnpm build` refused `ksor-lock-stale` naming a file `ksor build` cannot put in the lock, and the remedy that refusal prescribes wrote the identical lock. A symlinked asset hit the same disagreement and was reported as a stale lock rather than as the symlink it is. The stage now takes its assets from the record it already loaded, so there is one answer to what an asset is.
|
|
460
|
+
|
|
461
|
+
**The site checks the takedown ledger against git history, like the shipped checker does.** The lock is hand-editable and travels in the same change as the ledger, so on its own it cannot see an entry deleted: recomputing `ledger_sha256` and emptying `ledger_entries` made the two agree about a denial that was gone, and the denied document was published again. Outside a repository, or on a shallow clone, the build says so and falls back to the lock rather than refusing every shallow checkout.
|
|
462
|
+
|
|
463
|
+
**The all-draft build is tested by something that runs Next.** The fix for the route that used to throw when a build publishes no page was covered only by a staging test that never reaches the route module. The scaffold end-to-end suite now builds the starter exactly as `ksor init` emits it, before touching its policy, and asserts that not one draft reaches a page or `llms.txt`.
|
|
464
|
+
|
|
465
|
+
- d39e7c6: Close the review findings raised against the OKF-native record before it ships, most of them fail-open reads that no test would have caught.
|
|
466
|
+
|
|
467
|
+
**Governance objects are closed, not stripped.** The Governance Policy is the root of authority, and zod's default is to drop an unknown key — so `scope: { path: [...] }`, one letter wrong, left an empty scope that matches every concept and made a drafts-only rule the record's approval fallback. Every object in `.ksor/governance.yaml` now refuses an unknown key by name, with the nearest allowed one. The `ksor:` block is ksor's own namespace, not OKF's, and is closed the same way: `ksor.effective-from` (one hyphen) published an embargoed policy four weeks early with nothing red. A top-level key one edit from a profile key is refused too — a mistyped `stale_after` serves an expired document forever — while the concept's own top level stays open, as OKF §11 requires. And the keys the build writes into a document's twin (`trust_tier`, `build_id`, `source_commit`, `ksor_version`, `dirty`) are now refused on a concept: declaring one published it twice and made the build stamp forgeable.
|
|
468
|
+
|
|
469
|
+
**The site's lock covers the files that hold the governance.** Freshness was checked against documents and companions only, so deleting a denial's four lines from the ledger republished the denied document, and editing `instance.md` published a title nothing checked — exit 0, no slug. The three control files are hashed now, the lock's ledger entry digests are passed to the checker (so an entry retargeted in place refuses at the site build too), and assets join the lock, because the site publishes their bytes and a tampered diagram passed unnoticed. Three more fail-open reads in the same schema: an `as_of` that does not parse made every lifecycle comparison false and published a policy effective in 2030 as current; a `ksor_version` the site cannot compare slipped past the outdated gate and was stamped verbatim into every machine artefact; and a lock built with drafts SHOWN published every draft on a plain build.
|
|
470
|
+
|
|
471
|
+
**Refusals where the tool used to guess.** `ksor migrate` refuses `sor_id` (dropping it changes a document's stable id and breaks every takedown keyed on the old one), an escaping or stranded `superseded_by` (it used to write `ksor.superseded_by: null`, frontmatter the checker then rejects), a denylist row whose `scope` it cannot read, and a subtree row naming a document rather than a container. It deletes `id:` and `name:`, which only restated the path — leaving them made migrate an infinite loop that re-minted an approval instant on every run. A link from a public concept to an asset inside a restricted directory is `ksor-link-widens`: it used to stage that directory's name and bytes into the public build.
|
|
472
|
+
|
|
473
|
+
**The shipped gate can see a deleted takedown.** The emitted checker's only baseline was the committed lock, which travels in the same pull request: emptying both together printed "ok". It reads git history itself now, keeps the lock as a second baseline, and refuses a lock it cannot parse instead of silently holding nothing.
|
|
474
|
+
|
|
475
|
+
**A build that admits nothing still builds.** The docs route threw "the record has no documents" whenever a build published no page — the state a record of drafts reaches, which `ksor migrate` produces on its own whenever `--approve-by` is not passed. It renders the record it has instead of failing the build. The scaffold's own documentation is corrected in the same change: `ksor takedown --export` and `.ksor-denylist.json` are gone, the site honours a takedown from the committed ledger with no database access, and the dead denylist reader no longer ships in every adopter's repo.
|
|
476
|
+
|
|
477
|
+
- 48929da: **Review fixes across the record, the deny seam and the site's machine
|
|
478
|
+
surfaces.** Ten defects found by an independent review of the OKF-native work,
|
|
479
|
+
each reproduced before it was fixed.
|
|
480
|
+
|
|
481
|
+
Governance:
|
|
482
|
+
|
|
483
|
+
- A takedown ledger holding TWO denials of one document, with only the newer
|
|
484
|
+
revoked, disagreed with itself: the site kept the document withdrawn and the
|
|
485
|
+
MCP door served it. The denylist projection now takes its live set from the
|
|
486
|
+
same function the site reads, and `ksor takedown --list` no longer reports a
|
|
487
|
+
revoked row as denied.
|
|
488
|
+
- `ksor-link-widens` judged only links that resolved to a concept or an asset.
|
|
489
|
+
A public document could link a restricted document's `.summary.md`, a
|
|
490
|
+
restricted directory, or that directory's generated index — publishing the
|
|
491
|
+
restricted id and the directory name into the public page, its `/md/` twin
|
|
492
|
+
and `llms-full.txt`. Every target kind is judged now.
|
|
493
|
+
- A section whose every document had been taken down stayed in the door's
|
|
494
|
+
`outline` with `child_count: 0`, while the site pruned the folder entirely.
|
|
495
|
+
Denial binds inside the admission walk, so both surfaces refuse it. **This
|
|
496
|
+
moves the serving predicate's digest**, so a record with a calibrated
|
|
497
|
+
`vector_floor` refuses every `search`, `read` and `outline` with
|
|
498
|
+
`ksor-uncalibrated` until `ksor calibrate` is re-run. The door still boots,
|
|
499
|
+
and its report says so; the candidate set search sees is unchanged, but the
|
|
500
|
+
tripwire cannot know that.
|
|
501
|
+
|
|
502
|
+
Reading order — the site and the door disagreed three ways, and now share one
|
|
503
|
+
rule asserted row by row: folders interleave with documents instead of
|
|
504
|
+
following them, a folder sorts where its first document sorts however deep that
|
|
505
|
+
document is, and ties break on the file name on both surfaces. The starter's
|
|
506
|
+
`surfaces/` documents are renumbered so a fresh `ksor init` publishes exactly
|
|
507
|
+
the order it did before.
|
|
508
|
+
|
|
509
|
+
The site's machine surfaces:
|
|
510
|
+
|
|
511
|
+
- `` — the bundle-absolute image form the record checker
|
|
512
|
+
accepts — broke `pnpm build` with "Module not found" against a `public/`
|
|
513
|
+
directory the scaffold does not have. Images resolve against the stage now,
|
|
514
|
+
like every other bundle-absolute link.
|
|
515
|
+
- The `/md/` twin and `llms-full.txt` were built from fumadocs' processed
|
|
516
|
+
markdown, so an image reached them as `<img src="__img0" />` while the door
|
|
517
|
+
returned the record's own bytes. Both now republish the staged source.
|
|
518
|
+
- An image referenced only from a document's `.summary.md` was validated by the
|
|
519
|
+
checker, hashed into `build_id`, and never copied into the stage — killing
|
|
520
|
+
the export.
|
|
521
|
+
|
|
522
|
+
Operator surface: `ksor takedown --list` and `--ledger` work on a record with
|
|
523
|
+
no database, which is the rung `ksor init` emits — `--revoke` takes an id only
|
|
524
|
+
`--ledger` prints, so that workflow could not be completed at all. One bad
|
|
525
|
+
document no longer produces a cascade of `ksor-index-stale` refusals whose
|
|
526
|
+
prescribed fix cannot be run. And the takedown ledger's header no longer names
|
|
527
|
+
`pnpm` in npm and bun scaffolds.
|
|
528
|
+
|
|
529
|
+
- da1e5b6: Serving now reads the whole governance row, not just its audience half. Schema 2.5 stores the profile's lifecycle and trust on every node and, until now, only `audience` was consulted — so the MCP door answered from drafts, from documents before their `effective_from`, from documents past their `stale_after` and from deprecated ones, all four of which record spec §2.5 says a machine surface never publishes and all four of which the site's own build already refused. `lib/lifecycle.ts` and `lib/trust.ts` join the audience overlap in ONE admitted set that search's two arms, `read`, `outline` and the calibration sampler bind beside the takedown denial. A caller may name a minimum trust tier and it is enforced as an arm predicate, never after ranking. A SECTION carries no governance of its own and is admitted only when a descendant is visible, resolved by a recursive walk — so a folder whose every document is a draft, expired or below the floor stops advertising an empty shelf.
|
|
530
|
+
|
|
531
|
+
A calibrated `retrieval.vector_floor` now travels with `retrieval.floor_digest`, the digest of the retrieval predicate it was measured through, which `ksor calibrate` prints beside the number. A floor is a threshold inside one candidate set; carried across a predicate change it stays plausible and stops meaning what it said. A declared floor whose digest is not this door's — **including a floor calibrated before this release, which has none** — enters the existing declared-but-uncalibrated refusal, and the search envelope reports `gate: "uncalibrated"` rather than `"off"`. Re-run `ksor calibrate` and paste both lines. The snapshot token now binds the viewer list too, so a token minted for one audience cannot re-serve its pinned generation to another.
|
|
532
|
+
|
|
533
|
+
- 3d0de9f: Stage the scaffolded site on the OKF-native record (`specs/ksor/build/spec.md` §3). Staging now runs for EVERY build — the level-0 fast path that served `knowledge/` unstaged is gone, because no record is now safe to serve raw. The site reads the record with the SAME rules `ksor build` runs, through byte-copies of the record module (decision 18), so frontmatter is real YAML rather than a line scanner. `KSOR_AUDIENCE` is a comma list validated against the lock's registry (`ksor-viewer-unregistered`) and required to include `public` (`ksor-viewer-omits-public`); unset means `[public]`. Lifecycle is decided once, at the lock's `as_of`, by the record spec §2.5 table: drafts appear only under `pnpm dev` or `KSOR_DRAFTS=show`, and a deprecated, not-yet-effective or stale concept renders with a badge on the human surfaces and is absent from `llms.txt`, `llms-full.txt`, the markdown twins and `server.json`. Denials come from `.ksor/takedowns.yaml` in ledger order; the `.ksor-denylist.json` reader and its `ksor-denylist-missing` refusal are gone. Every directory's `index.md` is REGENERATED from the staged tree — never the committed one copied — so a folder page lists exactly what this viewer may see, and a directory with no admitted concept gets no bullet in its parent. `llms.txt`, `llms-full.txt`, every twin and `/.well-known/mcp/server.json` carry the build's `build_id`, `source_commit` and `ksor_version`. The site build refuses `ksor-lock-missing` / `ksor-lock-stale` without a fresh `build.lock.json` outside development, and `ksor-site-outdated` when the lock was written by a newer `ksor` than the site's rule modules carry. `pnpm dev` keeps the staging path with drafts admitted and machine routes stamped `build_id: null`, `unstamped: true`. Links between concepts now resolve in both the forms OKF §6.1 allows — bundle-absolute and relative, `.md` optional — which the shell's own resolver never read, so they reached the page they name rather than 404ing.
|
|
534
|
+
- 561750f: Show the whole trust signal on a governed page, and serve the markdown twin's frontmatter intact (`specs/ksor/record/spec.md` §2, `specs/ksor/build/spec.md` §3). A document's page now carries a **status chip on every page** — `draft`, `stable` or `deprecated`, `stable` included, because a reader who cannot see it cannot tell a governed record from a site that never said — the **trust tier** OKF names (`unverified` / `machine-confirmed` / `human-reviewed`) beside the verification that set it, the **approver and date** that make a stable document stable, and, on a withdrawn one, **who withdrew it and when**. Where the calendar keeps an otherwise current document off the machine surfaces, a second chip carries record spec §2.5's own words with the date the spec's ellipsis stands for: `effective from 2030-01-01`, `past its review date`. `unverified` is printed rather than hidden — it is the honest state of a stable, approved concept nobody has reviewed.
|
|
535
|
+
|
|
536
|
+
The `/md/` twins and `llms-full.txt` now serve each concept's **own frontmatter, verbatim**, under the derived `trust_tier` and the build's `build_id` / `source_commit` / `ksor_version`. The projection they replaced flattened `ksor.owner` into a top-level `owner:` and `ksor.effective_from` into `effective_from:` — both keys record spec §2.7 refuses BY NAME as pre-profile leftovers, so every twin published a frontmatter the record's own checker would have rejected, and every unknown key §2.7 preserves was dropped on the way out. An OKF consumer now parses the profile's grammar rather than this shell's summary of it.
|
|
537
|
+
|
|
538
|
+
Fixed: a withdrawn document's **search result** wore the ordinary grey chip, because the CSS that tints it still selected the pre-profile status word `superseded`. Search is where a reader chooses between two documents and where the snippet quotes the withdrawn figure.
|
|
539
|
+
|
|
540
|
+
- adf42df: The shipped docs and the scaffold's own contract now describe the OKF-native
|
|
541
|
+
record rather than the one before it.
|
|
542
|
+
|
|
543
|
+
Every document `ksor init` emits, and every page in the package's `docs/`, had
|
|
544
|
+
sentences that stopped being true when the record became an OKF bundle. The
|
|
545
|
+
ones an adopter would have acted on:
|
|
546
|
+
|
|
547
|
+
- **`ksor takedown` was documented as needing a database**, with examples that
|
|
548
|
+
omit the required `--actor`, pass `--subtree` (not a flag; the verb takes
|
|
549
|
+
`--scope subtree`) and pass a stable id to `--revoke` (which takes a ledger
|
|
550
|
+
entry id). None of the three commands ran. A takedown is ledger-first, so a
|
|
551
|
+
record with no database can withdraw a document, and the actor must be a
|
|
552
|
+
well-formed `human:`/`process:` id that `takedown_authorities` names.
|
|
553
|
+
- **`KSOR_AUDIENCE=<tier>` was documented in two places** and is refused: the
|
|
554
|
+
value is a comma list that must include `public`, and admission is list
|
|
555
|
+
overlap rather than a tier ordering.
|
|
556
|
+
- **The display title was documented as `instance.md`'s body `# H1`**, which
|
|
557
|
+
no longer exists; it is the `title:` key.
|
|
558
|
+
- **A summary companion was documented as carrying no frontmatter**, which is
|
|
559
|
+
now the one thing that refuses it: it carries exactly `type: Summary`.
|
|
560
|
+
- **`pnpm check` was credited with the quiz and slides audits.** Those run in
|
|
561
|
+
the site build; `pnpm check` never ran them.
|
|
562
|
+
- The tool-surface numbers in the scaffold's AGENTS.md were the 2026-08-23
|
|
563
|
+
measurement, taken before the trust floor and the per-hit governance block;
|
|
564
|
+
they are the re-measured ones, each with its date, and `min_trust_tier` is
|
|
565
|
+
now shown in the registration example it belongs to.
|
|
566
|
+
- The scaffold README's file table never named `.ksor/governance.yaml`,
|
|
567
|
+
`.ksor/takedowns.yaml` or `build.lock.json`, which are committed record
|
|
568
|
+
files an adopter has to understand.
|
|
569
|
+
|
|
570
|
+
`ingesting.md` also gains the remedy for a stale lock, which it never carried:
|
|
571
|
+
`ksor ingest` refuses `ksor-lock-stale` / `ksor-lock-missing`, the fix is always
|
|
572
|
+
`ksor build` and never an edit to the lock, and freshness covers seven sets —
|
|
573
|
+
the instance, the policy, the ledger, the concepts, the companions, the assets
|
|
574
|
+
and the generated indexes — so a refusal can name a file an adopter does not
|
|
575
|
+
think of as content.
|
|
576
|
+
|
|
577
|
+
No behaviour changed.
|
|
578
|
+
|
|
579
|
+
- 1d18eef: **The upgrade path from a pre-profile record now runs end to end.** Review of
|
|
580
|
+
the OKF-native release found `ksor migrate` rewriting the record and nothing
|
|
581
|
+
else, so an adopter who followed the runbook ended with a record they could
|
|
582
|
+
neither build nor check. What changed:
|
|
583
|
+
|
|
584
|
+
- **A record that declares a database can be migrated at all.** Reading its
|
|
585
|
+
takedown denylist went through a reader that accepts the profile's instance
|
|
586
|
+
only, so every record that had ever climbed to the served rung — exactly the
|
|
587
|
+
population with denials to transcribe — was refused before a single query
|
|
588
|
+
ran, blaming the database and telling you to run the command that had just
|
|
589
|
+
refused.
|
|
590
|
+
- **`ksor migrate` with no `--write` prints the diff again.** The documented
|
|
591
|
+
first step exited `1` on every pre-profile record, because the `--actor`
|
|
592
|
+
requirement did not distinguish showing a migration from applying one. The
|
|
593
|
+
dry run names `human:<you>` in the diff and says what to re-run with.
|
|
594
|
+
- **The commonest pre-profile shape no longer ends red.** A withdrawn document
|
|
595
|
+
pointing at an approved successor had that successor demoted to `draft`, and
|
|
596
|
+
`ksor build` then refused the tree. Migrate refuses that up front and names
|
|
597
|
+
`--approve-by`.
|
|
598
|
+
- **The files the migration invalidates are offered with it.** The emitted
|
|
599
|
+
format checker in both skill trees (a stale one refused the record migrate
|
|
600
|
+
had just written, in your editor and in your CI); the root `build` script,
|
|
601
|
+
which called a `ksor takedown` flag this release removed; `.gitignore`, whose
|
|
602
|
+
`.ksor/` line silently un-tracked the new Governance Policy and takedown
|
|
603
|
+
ledger; and, under `--write-site`, the WHOLE of `system/site` rather than
|
|
604
|
+
three rule modules.
|
|
605
|
+
- **`ksor build` refuses `ksor-governance-ignored`** when a policy or ledger it
|
|
606
|
+
can see is one git will never commit — the state that used to build green
|
|
607
|
+
locally and fail in a clone with a refusal that never named the cause.
|
|
608
|
+
- **`ksor build --strict` counts the build's own writes.** Regenerating a
|
|
609
|
+
committed-but-stale index made the tree dirty AFTER `dirty` was read, so a
|
|
610
|
+
strict build could stamp `dirty: false` and a commit that does not contain
|
|
611
|
+
what it published.
|
|
612
|
+
|
|
613
|
+
- 0a0b048: Two ways a concept could leave the index while its page stayed published.
|
|
614
|
+
|
|
615
|
+
**A `title` or `description` written across two lines is refused rather than rendered.** Both are written into ONE §8 index bullet, so a line break there does not render badly — it makes the bullet unreadable, and the concept disappears from the index, the sidebar and the reading order while it keeps its route and the MCP door keeps serving it. Nothing went red: the index generator and the index parser are two halves of one format and agreed on the broken output, so `ksor-index-stale` stayed green over it. A trailing break is the same defect wearing a YAML scalar style — `>` folds onto one line and keeps the newline, which emptied the description in the bullet and nowhere else. `ksor-one-line-form` now refuses both at the one place every surface reads, and `ksor migrate` folds a block or folded scalar onto one line rather than handing back a tree its own checker rejects.
|
|
616
|
+
|
|
617
|
+
**A `%` in a filename is refused, and the site no longer dies decoding one.** `knowledge/50%-off.md` passed the checker and then killed `next build` with a bare `URIError: URI malformed` naming no file at all. A path is also a URL, where `%` opens an escape sequence: `50%-off.md` is a malformed one and `50%20off.md` decodes to a different name, so the character gives one document two identities — which is what `ksor-name-unportable` exists to refuse. The site's decode is guarded as well, the way the record's own link resolver already guards the identical call, so a bundle from another OKF producer renders the listing it can instead of taking the build down.
|
|
618
|
+
|
|
619
|
+
- fe9a0af: Three findings from the third review pass, each one a guarantee that held only by accident.
|
|
620
|
+
|
|
621
|
+
**A governed document can no longer leave the record in silence.** A floor key that was PRESENT but unusable — `title: 42` from a title that lost its quotes, an empty `description:`, `status: 5` — pushed no refusal and had its schema complaint discarded as the duplicate of a refusal nobody had made. `parseConcept` returned "refused" with an empty list, so `ksor build` and `pnpm check` dropped the document — no page, no MCP node, no lock entry — printed nothing at all, and exited 0. Thirty-nine such values across the four floor keys did this; a whitespace-only title was accepted outright. Each is now refused by name, and a refusal with nothing to print is structurally impossible rather than merely absent.
|
|
622
|
+
|
|
623
|
+
**`order` must be a finite number.** YAML resolves `.inf`, `-.inf`, `.nan` and an overflowing `1e400` to real numbers, and the refusal an author got for one read "Invalid input: expected number, received number". It now says what an order is and what to write instead.
|
|
624
|
+
|
|
625
|
+
**A record that lives below its git repository root has a takedown ledger again.** `git show <rev>:<path>` reads a path relative to the repository root, while a `git log -- <pathspec>` is relative to the working directory — which is already the record root. Prefixing both asked git for `docs-sor/docs-sor/.ksor/takedowns.yaml`, and a pathspec that matches nothing is not an error: git exits 0 and prints nothing, so the baseline came back empty AND verified. Deleting a denial and the lock together then rebuilt clean and republished the withdrawn document. Now green on a record one and three directories down.
|
|
626
|
+
|
|
627
|
+
**The takedown ledger's history baseline is complete, or it says it is not.** Three ways a version could go missing while the answer still read "verified": git's default history simplification pruned a merged branch whose net effect on the file was nil, so a denial recorded and quietly withdrawn inside one pull request never entered the baseline and its deletion could never be caught — the one deletion the committed lock cannot catch either, because the lock travels in that same pull request. A ledger version larger than one megabyte was silently skipped, as was any version whose bytes failed to read, because a failed read looked exactly like the commit that deleted the file. All three are closed: the walk reads full history in ancestry order, a version that is in the tree but unreadable makes the whole baseline report itself unverified, and the size ceiling is 64 MB.
|
|
628
|
+
|
|
629
|
+
**`build.lock.json` records the generated indexes.** The `index.md` files a build writes are published — they are the surface an external reader parses to find anything at all — and they appeared in no section of the lock. They now have their own, hashed over the bytes the build wrote, and they move `build_id` like any other published bytes. Existing locks are regenerated by `ksor build`, which says so.
|
|
630
|
+
|
|
631
|
+
- 520f1ed: **`pnpm refresh` now builds before it ingests.** The emitted README gives one
|
|
632
|
+
ordered path to the agent surface — `pnpm provision`, `pnpm refresh`,
|
|
633
|
+
`pnpm serve` — and on a brand new record the second step failed:
|
|
634
|
+
`ingest` publishes only a tree `ksor build` has checked, and refused
|
|
635
|
+
`ksor-lock-missing` on a recipe that never mentions `ksor build`.
|
|
636
|
+
|
|
637
|
+
The refusal named the fix, so nobody was stranded — but the documented path did
|
|
638
|
+
not work, which is the thing a first run is for. `refresh` is
|
|
639
|
+
`ksor build && pnpm ingest && pnpm gc` now: publishing stays a deliberate act,
|
|
640
|
+
separate from serving, and the check that makes it publishable is part of it.
|
|
641
|
+
|
|
642
|
+
All three managers emit it: npm and bun REPLACE the manager-owned scripts
|
|
643
|
+
rather than extending the template's, so fixing the template alone left both of
|
|
644
|
+
them broken. Walked end to end under pnpm, npm and bun — install, provision,
|
|
645
|
+
refresh, serve, then a live MCP call returning cited hits.
|
|
646
|
+
|
|
647
|
+
**An existing record gets the same fix from `ksor migrate`.** The sentence
|
|
648
|
+
here used to say existing records needed no change, which was exactly backwards:
|
|
649
|
+
`ksor ingest` gained a lock gate in this release, so a `refresh` that does not
|
|
650
|
+
build first refuses `ksor-lock-stale` the first time the record is edited
|
|
651
|
+
(`ksor-lock-missing` if `ksor build` has never run). `migrate` rewrites the
|
|
652
|
+
script — matched by the ingest script it calls, so it works whichever manager
|
|
653
|
+
scaffolded the project, and the `ingest` script itself is left alone.
|
|
654
|
+
|
|
655
|
+
If you upgraded before this release and hand-edited your manifest, check that
|
|
656
|
+
`refresh` begins with `ksor build &&`.
|
|
657
|
+
|
|
658
|
+
- 4c95a7a: **Fix a release gate that broke on the act of releasing.** Four doc-truth
|
|
659
|
+
assertions read `.changeset/<slug>.md` directly. A changeset is a transient
|
|
660
|
+
file — `changeset version` folds it into `CHANGELOG.md` and deletes it — so
|
|
661
|
+
those assertions passed on every feature PR and threw `ENOENT` in the Version
|
|
662
|
+
PR, the one run whose failure costs a red release instead of a red PR. It would
|
|
663
|
+
have done so on every future release, not just this one.
|
|
664
|
+
|
|
665
|
+
The assertions were right; only the place they looked was wrong. A new
|
|
666
|
+
`releaseNote()` resolves a note to the pending changeset when it is still
|
|
667
|
+
pending, and otherwise to the newest section of the changelog it was folded
|
|
668
|
+
into — scoped to the newest section deliberately, so a rule adopted in this
|
|
669
|
+
release is never asserted against prose written several releases ago.
|
|
670
|
+
|
|
671
|
+
- dc079c5: `ksor ingest --knowledge` is retired. The record root — the directory holding
|
|
672
|
+
`instance.md` — supplies `knowledge/`, `.ksor/` and `build.lock.json` alike
|
|
673
|
+
(record spec §1), so the flag could only ever name the one directory it was
|
|
674
|
+
already going to read. It survived this release as a tolerated argument that
|
|
675
|
+
`--help` did not list, which is the shape of a trap: it worked, so nobody
|
|
676
|
+
noticed it meant nothing.
|
|
677
|
+
|
|
678
|
+
Passing it now refuses like any other unknown flag, and `ksor migrate` strips
|
|
679
|
+
it from the `ingest` script the pre-profile scaffold shipped, in the same diff
|
|
680
|
+
that drops `export-denylist`.
|
|
681
|
+
|
|
682
|
+
- aec9ddd: **The emitted scaffold now says what a fresh record publishes, and what its
|
|
683
|
+
tooling actually does.** Six sentences an adopter acts on were false about the
|
|
684
|
+
project `ksor init` hands them.
|
|
685
|
+
|
|
686
|
+
- **The record now says what a fresh build publishes.** The emitted README
|
|
687
|
+
and AGENTS.md said nothing about the starter's publication state at all, and
|
|
688
|
+
the `intake-interview` skill never raised it. All three now do, and the
|
|
689
|
+
skill's turn is offered rather than performed on the owner's behalf, never
|
|
690
|
+
beside an invented `verified` entry. (Which state they describe moved in the
|
|
691
|
+
same release — see "A freshly scaffolded record now publishes on its first
|
|
692
|
+
build".)
|
|
693
|
+
- **`ksor takedown --list` and `--ledger`** were documented as needing
|
|
694
|
+
"nothing". They need no ACTOR; the sentence now says that, and says they
|
|
695
|
+
read the committed `.ksor/takedowns.yaml` on a record with no database.
|
|
696
|
+
Both AGENTS.md and `docs/ingesting.md` also presented `--ledger` as the only
|
|
697
|
+
route to the entry id `--revoke` takes — the denial prints it and the ledger
|
|
698
|
+
file holds it, and neither needs a database.
|
|
699
|
+
- **The format-checker skill** claimed "a ksor upgrade replaces it" of a
|
|
700
|
+
`check.mjs` that no verb refreshed, in a skill that tells the agent to obey
|
|
701
|
+
a printed fix literally. It now names the upgrade path and the rule for
|
|
702
|
+
when checker and record disagree: upgrade, never undo the migration.
|
|
703
|
+
- **`.env.example`** told npm and bun adopters to set three variables "before
|
|
704
|
+
`pnpm build`". It is the one emitted file copied byte-for-byte rather than
|
|
705
|
+
prose-translated, so it now names no manager at all.
|
|
706
|
+
- **The `## Skills` list** had lost a sentence to an inserted bullet:
|
|
707
|
+
`make-slides` ended mid-sentence and `make-summary` read "attach it and
|
|
708
|
+
attach it".
|
|
709
|
+
- **The actor convention** is documented as far as it is enforced.
|
|
710
|
+
`ksor.owner` is free text that nothing parses — every other actor slot is
|
|
711
|
+
form-checked — so the profile documentation says so instead of describing a
|
|
712
|
+
check that does not run.
|
|
713
|
+
|
|
714
|
+
The upgrade runbook's preview step now names `--actor human:<you>`. Bare
|
|
715
|
+
`ksor migrate` does print the diff — it writes nothing, so it needs nobody's
|
|
716
|
+
name — but the `.ksor/governance.yaml` in that diff carries a `human:<you>`
|
|
717
|
+
placeholder where your handle will go, and passing your own shows the file you
|
|
718
|
+
will actually get. `--write` is the step that refuses without `--actor`,
|
|
719
|
+
because that is the step that performs the act.
|
|
720
|
+
|
|
721
|
+
- 71309c2: **Cuts and corrections from the final simplification pass.** Nothing here
|
|
722
|
+
changes what the tool does; it removes code that had stopped being reachable
|
|
723
|
+
and corrects three comments that had stopped being true — plus one hand copy
|
|
724
|
+
of a rule that was actively wrong.
|
|
725
|
+
|
|
726
|
+
- **`page-order.ts` is gone**, both copies. It sorted the site's page tree
|
|
727
|
+
until this release replaced that path with the generated indexes, after which
|
|
728
|
+
its only remaining callers were the drift tests asserting the two copies
|
|
729
|
+
matched — a guard on dead code. The tie-break reasoning it recorded (a
|
|
730
|
+
folder's descendant url compares against a sibling in the wrong order,
|
|
731
|
+
because `/` sorts after `-`) moves onto the live `routeAt`, which performs
|
|
732
|
+
the defence and did not say why.
|
|
733
|
+
- **The attachment suffix list had a fifth hand copy**, in the site's staging
|
|
734
|
+
step, and it was wrong: it claimed byte-identity with a checker that had
|
|
735
|
+
moved to the canonical rule, and it was missing `.summary.mdx`, so the stage
|
|
736
|
+
and the lock writer disagreed about which files are attachments. Both
|
|
737
|
+
divergences were masked by earlier refusals, so nothing was observable — the
|
|
738
|
+
shape decision 18 exists to catch. It now calls the canonical rule, as does a
|
|
739
|
+
sixth copy found in a test fixture, where a fixture classifying attachments
|
|
740
|
+
by its own rule could not detect the code under test classifying them
|
|
741
|
+
differently.
|
|
742
|
+
- **The guard that should have caught those two** scanned one directory and
|
|
743
|
+
skipped test files. A copy was sitting in each blind spot. It now covers the
|
|
744
|
+
emitted scaffold and tests as well.
|
|
745
|
+
- **`splitFrontmatter` in the ingest module is gone.** Decision 26 made
|
|
746
|
+
`record/frontmatter.ts` the one reader and every caller moved; what was left
|
|
747
|
+
behind was an unused copy whose test asserted three behaviours the product
|
|
748
|
+
had deliberately abandoned.
|
|
749
|
+
- Also removed: three exported helpers in the site's attachment module that
|
|
750
|
+
never had a caller, a predicate parameter no caller ever passed, and a
|
|
751
|
+
test that asserted `true`.
|
|
752
|
+
|
|
753
|
+
**Two guarantees gained an assertion**, both found while looking for
|
|
754
|
+
redundancy rather than for holes:
|
|
755
|
+
|
|
756
|
+
- **The trust tier now has a conformance table.** It had two implementations —
|
|
757
|
+
the kernel's and the site's, which cannot import the kernel — and nothing
|
|
758
|
+
asserting they agree, while the tier is stamped into every `/md/` twin and
|
|
759
|
+
stored as the column the MCP door's `min_trust_tier` floor compares against.
|
|
760
|
+
Both halves now run the same rows, including one the previous hand-written
|
|
761
|
+
expectations did not cover: an actor whose producer merely contains the word
|
|
762
|
+
`human` is a machine.
|
|
763
|
+
- **`GATE_PREDICATE_DIGEST` is pinned by value, not by shape.** It was asserted
|
|
764
|
+
only to be twelve hex characters. Every `ksor calibrate` writes it into the
|
|
765
|
+
adopter's `instance.md` and the door compares it at boot, so a whitespace-only
|
|
766
|
+
reflow of the serving predicate would have invalidated every calibrated floor
|
|
767
|
+
in the field with nothing going red.
|
|
768
|
+
|
|
769
|
+
- 0a0b048: Teach the record checker about carried pages, so a document may ship the sim it frames.
|
|
770
|
+
|
|
771
|
+
A record can carry an interactive page beside its document — `<name>.sim.html`, framed click-to-load where the prose puts it, served from the record's own path so it works offline and no third party learns who is reading. The checker had never been told: it refused every one of them `ksor-file-type` ("unexpected file type `.html`"), so `ksor build` exited 1 on any record that used the feature and no adopter could publish a sim at all.
|
|
772
|
+
|
|
773
|
+
`<name>.sim.html` is now admitted, by that SUFFIX and nothing wider — a bare `.html` or `.htm` is still refused, and now says what shape a carried page has to take instead of only that this one is wrong. The rule that decides it is one file (`lib/sim-rule.ts`), read by the checker, the site's staging and the emitted `pnpm check` alike, and pinned to the site's embed rule by a test, because a marker that drifts between "what the record admits" and "what the site frames" fails silently in both directions.
|
|
774
|
+
|
|
775
|
+
A sim stays an ASSET, not a study attachment: named freely, many per document, no route, no stable id, no MCP node, no `llms.txt` line and no markdown twin of its own — asserted now rather than assumed. Its governance is inherited by position, through the link in a document that survived every filter: an internal document's sim reaches no public build, a taken-down document's sim is denied with it under node and subtree denials alike, and a sim no document links is never published, so it never becomes a url.
|
|
776
|
+
|
|
777
|
+
- ac1c477: **A restricted document's sim no longer survives into a public build.** The
|
|
778
|
+
site published carried sims by copying them into `system/site/public/sims/`
|
|
779
|
+
and never removing what an earlier build had left there. Because static export
|
|
780
|
+
ships `public/` verbatim and that directory is gitignored, the accumulation was
|
|
781
|
+
invisible and served: build once with `KSOR_AUDIENCE=public,internal`, build
|
|
782
|
+
again with the default, and the internal document's sim was still at
|
|
783
|
+
`/sims/<path>.html` — the same for a document taken down between builds.
|
|
784
|
+
|
|
785
|
+
The staged tree was correct in both builds, which is why nothing was red; the
|
|
786
|
+
leak was entirely in the publish step that mirrors it. That step now prunes
|
|
787
|
+
whatever the current build did not publish, so `public/sims/` holds exactly the
|
|
788
|
+
sims this audience is allowed to see. Adopters get it on the next build; no
|
|
789
|
+
record change is needed.
|
|
790
|
+
|
|
791
|
+
- cd21ec3: A code fence in a language the highlighter does not carry renders as plain
|
|
792
|
+
text instead of failing the build. A record is not a code project: an author
|
|
793
|
+
writing ` ```promql `, ` ```logql ` or ` ```gotemplate ` is describing their own
|
|
794
|
+
stack, and shiki throws on a language it has no grammar for — so one fence
|
|
795
|
+
anywhere in the record took the whole site down with a stack trace naming a
|
|
796
|
+
file in `node_modules`. Found on a real 187-document handbook where three such
|
|
797
|
+
languages appeared across some 3,000 fences.
|
|
798
|
+
- fe9a0af: Seven site fixes from the 2026-08-25 review.
|
|
799
|
+
|
|
800
|
+
**The stage stopped re-writing itself, and a companion stopped escaping its parent.** A staged path is now emitted once, whatever asks for it. A document linking its own deck (`[Cards](./x.flashcards.yaml)`) had it copied twice — once as its parent's companion and once as a link target found on disk — and the freshness check that decides whether a stage may be left alone compares a count before it compares bytes, so it answered false forever in that record: every evaluation of every build wiped and refilled a stage that was already correct, which is exactly the check that stands between a build and a half-written stage. The same filesystem probe was a second way into the stage for a file that is meant to have only one: a public document linking a TAKEN-DOWN document's deck staged the deck, because the link rules judge a companion by its parent's audience and a takedown is not an audience. A companion now reaches the stage with its parent or not at all.
|
|
801
|
+
|
|
802
|
+
**`site.governance: false` no longer swallows a lifecycle caveat.** A `stable` document with a future `ksor.effective_from`, or one past its `stale_after`, showed its chip in the sidebar, in folder listings and in search — and then opened as a current, in-force policy with nothing on the page, while the MCP door declined it outright. The key hides ATTRIBUTION (owner, approver, verifier, sources); it never hid the deprecation notice, and it now never hides the badge either. `deprecated` stays off the page's chip row, because the notice above the title already says it.
|
|
803
|
+
|
|
804
|
+
**A file the record may not hold is diagnosed by the rule it breaks.** An `.mdx` dropped into `knowledge/` was reported as `ksor-lock-stale` — "run `ksor build` again", which is the command that refuses the file — instead of `ksor-file-type`. The lock's file-by-file comparison now runs after the record's own checker, so the tree is judged a record before the lock is asked whether it describes that tree. Same for bytes that are not a valid image, now `ksor-asset-corrupt`.
|
|
805
|
+
|
|
806
|
+
**A link's scheme is read the way a browser reads it.** `\tjavascript:…` fell through the site's scheme test, which then treated it as a link into the record. Defence in depth — the record checker refuses such a link before the page exists — but the guard now strips the leading control characters a URL parser strips, so it means what it says.
|
|
807
|
+
|
|
808
|
+
**The record watcher stopped holding the process open.** `pnpm dev` watches `knowledge/` and unref'd the watcher so it could never be the reason a process refuses to exit. On macOS and Windows that works — a recursive watch is native there. Everywhere else, Node substitutes a JS implementation that opens one watcher per directory and whose `unref()` walks a map of `Stats` objects unrefing anything that is `instanceof StatWatcher` — nothing in that map ever is, so it is a silent no-op and every watcher, created persistent by default, holds the event loop open forever. The watcher is now declared `persistent: false`, which is what makes the promise true on every platform; it still delivers every event while the dev server holds the process open, which is the only time it runs.
|
|
809
|
+
|
|
810
|
+
**The stage lock stops waiting.** A holder killed mid-stage — Ctrl-C on `pnpm dev`, a cancelled job, an OOM — never runs the code that removes its lock, and the waiter broke a lock only when the recorded pid was GONE. A recycled pid reads as alive, so the wait was unbounded in practice as well as in code: `pnpm dev` repeating "waiting on .staged-knowledge.lock" on every request, with no build running, until the file was deleted by hand. After two minutes the build now refuses `ksor-stage-locked`, naming the file, the recorded pid, and what a signal-0 probe actually established — including that EPERM means "exists and is not ours", which a recycled pid produces too. It does NOT break the lock: the stage is removed and refilled in place, so breaking one a live holder still holds would publish a half-written record.
|
|
811
|
+
|
|
812
|
+
**The site's lock reader covers the indexes.** `build.lock.json` records the bytes of every `index.md` the build generated, and `ksor ingest` has always compared them; the site's reader declared three lists and compared three. The indexes are the one thing under `knowledge/` the build writes rather than reads, and the one thing the site never copies — it regenerates a per-viewer set — so a committed index left at another branch's bytes by a merge, with no re-run of `ksor build`, was invisible to `pnpm build` and `ksor-lock-stale` to the door: one surface publishing what the other refused. The comparison is against the COMMITTED bytes, never the staged ones, because the lock records the whole record's indexes and a restricted viewer's index is legitimately shorter.
|
|
813
|
+
|
|
814
|
+
- 35e2cdf: **A freshly scaffolded record now publishes on its first build.** `ksor init`
|
|
815
|
+
then `ksor build` reports **5 admitted to a machine surface**. It reported
|
|
816
|
+
**0**: the five starter documents shipped `status: draft`, and a draft reaches
|
|
817
|
+
no surface of a build — so a brand-new project came up with an empty
|
|
818
|
+
`## Documents` in `llms.txt`, empty `/md/` twins, no document route, and an MCP
|
|
819
|
+
door that answered nothing. That was deliberate, and it cost more than it was
|
|
820
|
+
worth on the one build that is meant to be simple to get started.
|
|
821
|
+
|
|
822
|
+
**What changed.** The five samples ship `status: stable` with
|
|
823
|
+
`ksor.approval: { by: "ksor-starter/<the CLI version that scaffolded you>" }`,
|
|
824
|
+
and the emitted `.ksor/governance.yaml` authorises that actor beside
|
|
825
|
+
`human:you`.
|
|
826
|
+
|
|
827
|
+
**The approver is a producer, not a person.** `ksor-starter/0.0.x` is the same
|
|
828
|
+
form `generated.by` already uses, so it can never be read as a human who
|
|
829
|
+
reviewed something — which is exactly what the rule against a tool recording an
|
|
830
|
+
approval exists to prevent. The trust tier on every one of those pages stays
|
|
831
|
+
`unverified`, and no `verified` entry is written. Your record does not claim
|
|
832
|
+
anybody checked this, because nobody did.
|
|
833
|
+
|
|
834
|
+
**Two things to do with the samples.** They describe KSoR, not your
|
|
835
|
+
organisation, so replacing them is the first real act on the record — and when
|
|
836
|
+
the last one is gone, delete `ksor-starter/...` from `approval_authorities` in
|
|
837
|
+
`.ksor/governance.yaml`. Nothing of yours should be approved by a tool. The
|
|
838
|
+
emitted README, `AGENTS.md`, the policy file's own comment and the
|
|
839
|
+
`intake-interview` skill all say so.
|
|
840
|
+
|
|
841
|
+
**Nothing changes for what you write.** A new document is `status: draft` and
|
|
842
|
+
reaches no machine surface — no page, no sidebar row, no `llms.txt` entry —
|
|
843
|
+
until a human approves it with `status: stable` plus a
|
|
844
|
+
`ksor.approval: { by, at }` naming an actor your policy authorises.
|
|
845
|
+
|
|
846
|
+
**Existing records are untouched.** This is the `ksor init` template only; no
|
|
847
|
+
verb, refusal or lock field changed, and `ksor migrate` still demotes
|
|
848
|
+
`approved` to `draft` unless `--approve-by` names the human approving.
|
|
849
|
+
|
|
850
|
+
- 959c8c5: **A subtree takedown now reads `expected`, so one ordinary command no longer wedges the record.** `ksor takedown --scope subtree knowledge/<dir>` on a directory that does not exist yet is a sanctioned act — a denial may precede what it names — and the verb recorded it correctly, printing `expected: removed` and exiting 0. The checker then refused it: its subtree branch judged presence alone and never consulted `expected`, so the very next `ksor build` exited 1 with `ksor-takedown-dangling`, on a ledger entry that is append-only and cannot be deleted. The only escape it named was `--revoke`, which records a lift that never happened and drops the hold if the path ever returns; "restore the directory" does not survive a clone, because git cannot commit an empty one. The identical act at node scope built green. The sanctioned withdraw-then-delete sequence wedged the same way, and `ksor migrate --write` could produce a ledger whose first build refused, because it hardcoded `expected: present` for every subtree denial it transcribed.
|
|
851
|
+
|
|
852
|
+
The mirror gap was the same defect facing the other way: `expected: removed` at subtree scope had no re-added arm, so a directory the record said was deleted could come back with nothing red — while the serving half had read `expected` scope-blind all along, meaning the two surfaces disagreed about which records are publishable. One rule now decides both scopes, in both directions: a `present` entry whose target is gone is `ksor-takedown-dangling` and a `removed` entry whose target is back is `ksor-takedown-readded`, whether the target is a document or a directory. `ksor migrate` derives `expected` from the post-migration tree with the same function the checker judges it with, and the dangling refusal names `--removed` — the exit that records what actually happened — rather than a revocation. The record ROOT (`knowledge/#section`) is still refused whatever `expected` says: that form is unhonourable by the serving half, not merely out of step with the tree.
|
|
853
|
+
|
|
854
|
+
- 8a384be: Two ways `ksor takedown` answered wrongly about arguments an adopter types.
|
|
855
|
+
|
|
856
|
+
**`ksor takedown --instance .` reads the record you are standing in.** It was the last verb taking `--instance` verbatim, so it resolved the record root as `dirname()` of the path given — the record's PARENT when the path is a directory. The verb then reported `ksor-policy-missing` about a record whose `.ksor/governance.yaml` was right there, and the fix it printed would have had the adopter overwrite their real `approval_authorities` and `takedown_authorities`: a false report about the record, with a remedy that destroys governance. `--list` and `--ledger` hit the same cause down a different path — reading a directory throws `EISDIR` rather than "declares no database", so the level-0 branch was never taken and a record with no database was told to stand up Postgres. Every verb now shares the one `--instance` rule the usage has always documented: an `instance.md`, or a directory at or below the record root.
|
|
857
|
+
|
|
858
|
+
**A stable_id is recorded as the record spells it, not as the shell completed it.** A trailing slash is never part of a concept id, and a shell puts one on every directory it completes. `ksor takedown knowledge/policies/x/` matched no concept, so both surfaces denied nothing — and it recorded `expected: removed`, which agrees with "no such concept", so the checker stayed green and nothing ever said the hold was fake: a governance act reported as done, with no red anywhere. `--scope subtree knowledge/policies/#section` recorded the directory `policies//`, which every later `ksor build` refuses in an append-only ledger. The slash is now normalized away on both sides of the `#section` anchor before anything reads the id.
|
|
859
|
+
|
|
860
|
+
**A denial naming the whole record is refused as an act, not left in the ledger.** `ksor takedown --scope subtree knowledge/` crashed with a raw `TypeError` under exit `3` — the ENVIRONMENT code, for a bad argument — and `knowledge/#section` was worse: it exited `0` and wrote an entry that every later `ksor build` refuses, in a ledger that is append-only. At the default scope the same id matched no concept, so both surfaces denied nothing while the verb reported a denial. All four spellings are now `ksor-takedown-record-root` at either scope, before anything is written, carrying the same reasoning and the same remedy as the checker's refusal of the recorded form — one subtree entry per top-level section — from one shared text.
|
|
861
|
+
|
|
862
|
+
- 8a384be: **`ksor takedown` no longer destroys another operator's withdrawal, and no longer reports one it did not record.**
|
|
863
|
+
|
|
864
|
+
The verb read `.ksor/takedowns.yaml`, decided what the act was, and wrote the WHOLE file back. Nothing serialised those three steps, so a second run that read before the first one wrote rewrote the file from its own stale text — and every entry appended in between was gone. Each run printed ``recorded as `<id>``` and exited `0` all the same, because its own write had succeeded; what it deleted was somebody else's act. Reproduced on a stock `ksor init` scaffold with no database at all: five concurrent runs, five claims of success, three entries on disk, and two documents still published with nothing anywhere in the record saying anyone had ever asked for them to be withdrawn. `ksor build` was green over it, because a ledger that never held the entry is indistinguishable from one whose author never wrote it.
|
|
865
|
+
|
|
866
|
+
The same line had a worse reading. `writeFileSync` opens with `O_TRUNC`, so the ledger is ZERO BYTES for the width of the write, and that window is reachable by any concurrent reader — 3.3% of reads under sustained contention on a real 7 KB ledger, and once in 5,177 reads sampled while ordinary `ksor takedown` processes ran. An empty file parsed as a valid EMPTY LEDGER, so the verb handed that state believed the record had withdrawn nothing and wrote a one-entry ledger over forty, exit 0. There is no restore verb, and the entry a revocation would have to name is one of the ones that was deleted, so every remedy the resulting refusals print dead-ends.
|
|
867
|
+
|
|
868
|
+
Two mechanisms now, because they answer different failures. The read, the decision and the write happen inside an exclusive lock (`.ksor/takedowns.yaml.lock`, created with `wx` and stamped with the holder's pid; a lock whose holder is gone is broken, and one still held after 30s refuses `ksor-ledger-locked` under exit `3` having written nothing and claimed nothing) — that is what makes N concurrent acts produce N entries. And the write is an `O_APPEND` of the new entry ALONE, which is what makes the loss impossible rather than merely unlikely: a lock is a convention, and an append survives a writer that ignores one. The file now has no state in which it is shorter than it was.
|
|
869
|
+
|
|
870
|
+
**A ledger file that exists and is empty is refused (`ksor-ledger-empty`) instead of read as "this record has withdrawn nothing".** No writer produces one — the verb writes the header and the first entry in the same call — so an empty ledger is not a record without denials, it is a record whose denials were lost, and the refusal is the one moment at which they can still be restored from version control. Absence of the FILE is still the honest way to say nothing has ever been withdrawn, and a fresh `ksor init` scaffold ships exactly that.
|
|
871
|
+
|
|
872
|
+
## 0.0.40
|
|
873
|
+
|
|
874
|
+
### Patch Changes
|
|
875
|
+
|
|
876
|
+
- cd42f81: Three readability changes to a document's page.
|
|
877
|
+
|
|
878
|
+
A long line in a code block can be **unwrapped by the reader**, with a button
|
|
879
|
+
that appears only on a block that actually overflows. The record's commands run
|
|
880
|
+
to hundreds of characters, and in a docs column that was a scrollbar with the
|
|
881
|
+
left edge of every line going out of view. Wrapping is not the default, because
|
|
882
|
+
it is worse for the blocks that do not need it.
|
|
883
|
+
|
|
884
|
+
A table's rows **alternate**, faintly, so a row holds together across three
|
|
885
|
+
columns of wrapped prose.
|
|
886
|
+
|
|
887
|
+
A callout carries a **rule down its left edge**, in its kind's colour — the
|
|
888
|
+
same device a verbatim block uses, so "this passage is set apart" looks the
|
|
889
|
+
same everywhere it happens.
|
|
890
|
+
|
|
891
|
+
- e19f226: A passage a reader must not miss can now be a callout. Write GitHub's alert
|
|
892
|
+
syntax — a blockquote whose first line is `[!WARNING]` — and the site renders a
|
|
893
|
+
styled panel tinted with that kind's own colour:
|
|
894
|
+
|
|
895
|
+
```markdown
|
|
896
|
+
> [!TIP]
|
|
897
|
+
> Climb one rung at a time, and only when something has gone wrong without it.
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
It stays a plain blockquote everywhere else the record is read, carrying a
|
|
901
|
+
visible label, and `/md/` and `llms-full.txt` keep the author's blockquote
|
|
902
|
+
rather than this site's component.
|
|
903
|
+
|
|
904
|
+
Not `:::warning`: a dialect renders as literal punctuation everywhere outside
|
|
905
|
+
this site.
|
|
906
|
+
|
|
907
|
+
- 704e949: A document can now show something running, where the prose puts it. Give a link
|
|
908
|
+
the title `embed` and the site renders it as a click-to-load frame:
|
|
909
|
+
|
|
910
|
+
```markdown
|
|
911
|
+
[Play run-until-done](goal-loop.sim.html "embed")
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
It stays an ordinary CommonMark link everywhere else — GitHub, a plain editor,
|
|
915
|
+
`/md/`, `llms-full.txt` — so nothing was added to `knowledge/`.
|
|
916
|
+
|
|
917
|
+
Prefer carrying the page in. A `<name>.sim.html` beside its document, exactly
|
|
918
|
+
like a figure, is published by the build and served from your own site: it
|
|
919
|
+
works offline, tells nobody outside what someone is reading, and is versioned
|
|
920
|
+
with the document. An `https:` link works too, but many hosts send
|
|
921
|
+
`X-Frame-Options: SAMEORIGIN` and a browser will refuse to frame them.
|
|
922
|
+
|
|
923
|
+
Nothing is requested until a reader clicks, so a built page still makes zero
|
|
924
|
+
external requests. A carried page is measured, so the frame is exactly as tall
|
|
925
|
+
as what it holds — you never write a height into a document.
|
|
926
|
+
|
|
927
|
+
- a55e93e: A numbered list now counts in the record's accent, and the term a list item
|
|
928
|
+
defines takes it too — `**Heartbeat:** a schedule that…` puts the term in
|
|
929
|
+
accent and leaves the sentence in ink. The markers were grey at regular weight,
|
|
930
|
+
lighter than the text they counted, so a list of six steps read as six
|
|
931
|
+
paragraphs that happened to start with digits.
|
|
932
|
+
- a59a7af: The documented way to run `ksor init` pins a version, so a stale runner cache
|
|
933
|
+
can no longer decide which ksor an adopter meets.
|
|
934
|
+
|
|
935
|
+
`npx @panaversity/ksor init my-sor` is spec `*`, and any cached version
|
|
936
|
+
satisfies it — so npx runs whatever that machine already has without consulting
|
|
937
|
+
the registry. Found live on a Windows box following the README: it replayed
|
|
938
|
+
`0.0.0`, the name-reservation stub published on the first day of the project,
|
|
939
|
+
whose whole implementation prints "the name is reserved; this is not a release"
|
|
940
|
+
and exits 2. Thirty-nine releases later, the first command in the README
|
|
941
|
+
produced a placeholder, and nothing in that output points at the cause.
|
|
942
|
+
|
|
943
|
+
Both READMEs now say `@panaversity/ksor@latest`. The three "Start here" forms
|
|
944
|
+
change together — `pnpm dlx` reuses its cache for 24 hours by default and
|
|
945
|
+
`bunx` resolves from the install cache before the registry, so pinning only npx
|
|
946
|
+
would have left two of the three supported managers in the trap. `npm install
|
|
947
|
+
-g` is unchanged: an install resolves the `latest` dist-tag by definition.
|
|
948
|
+
|
|
949
|
+
If you have run ksor before, your own cache is still warm. Run the `@latest`
|
|
950
|
+
form once and it resolves the current release.
|
|
951
|
+
|
|
952
|
+
- 9e92e9e: A document with no summary no longer carries an empty view strip. The reading
|
|
953
|
+
time moved into the governance row, beside the owner and the effective date,
|
|
954
|
+
so the commonest document — one with no summary — gets one bar of facts rather
|
|
955
|
+
than a second bar holding a single number at the far end of a rule.
|
|
956
|
+
- d723487: A table's head now reads as a head: the record's accent as a wash behind it,
|
|
957
|
+
the column labels in that accent's ink, and an accent rule under the band. They
|
|
958
|
+
were grey on near-white, so on a three-column table the eye had to work out
|
|
959
|
+
which row was the label.
|
|
960
|
+
- 8ec1a10: A fenced block with no language is set as a passage to reproduce rather than as
|
|
961
|
+
code: prose leading, and the record's accent down its left edge. Prompts,
|
|
962
|
+
commands to say and messages to paste were arriving as walls of tight
|
|
963
|
+
monospace, set for scanning columns of code when what is in them is sentences.
|
|
964
|
+
|
|
965
|
+
Nothing to author — the highlighter's own output is the signal, so a block that
|
|
966
|
+
was never highlighted is the one that changes.
|
|
967
|
+
|
|
3
968
|
## 0.0.39
|
|
4
969
|
|
|
5
970
|
### Patch Changes
|