ksor
panaversity/ksor/AGENTS.md
The durable contract for working in this repository: what ksor is, the vocabulary, the decisions, the invariants, and how it is built and tested. Loaded every session, so it holds only what stays true — what is true this week lives in docs/status.md; the product pitch lives in README.md, its only home. CLAUDE.md is a symlink to this file. They are the same file: one contract for every agent — human-readable and agent-readable, like everything else here. 1. Never weaken…
AGENTS.md219 starsChanged 37 days ago
- Reads credentials
- Installs packages
# AGENTS.md
The durable contract for working in this repository: what ksor is, the
vocabulary, the decisions, the invariants, and how it is built and tested.
Loaded every session, so it holds **only what stays true** — what is true this
week lives in [`docs/status.md`](docs/status.md); the product pitch lives in
[`README.md`](README.md), its only home.
> CLAUDE.md is a symlink to this file. They are the same file: one contract for
> every agent — human-readable and agent-readable, like everything else here.
## Critical rules
1. **Never weaken provenance, citation, abstention, or governance guarantees to
simplify an implementation.** They are the product, not features of it.
2. **Never push directly to `main`.** Every change lands through a pull request.
3. **Never break the agent-discoverable surfaces**: docs bundled in the npm
package (`packages/ksor/docs/`), and — once the site ships — its `llms.txt`
and `/.well-known/mcp/server.json`. Agents finding ksor is how ksor gets used.
## What this is, in one line
A CLI (`ksor` — the npm package is `@panaversity/ksor`) that compiles a folder
of governed markdown into two surfaces — a static website for people and an MCP
server for AI agents — with cited answers and honest abstention. It is not an
agent framework; it is the knowledge layer agent frameworks read from.
**Which verbs are implemented is not recorded here.** This file describes what
ksor _is_; `docs/status.md` holds what is built this week. One rule keeps the
CLI itself the current answer: an unimplemented verb says so and exits `2`, an
unknown word is refused with exit `1` — so no document has to be kept in step
with the binary.
A Python-era predecessor (vsor, `panaversity/zia-vsor-sdk`) proved much of the
design. Its work may be taken and converted to TypeScript (decision 6), but it
is a source to mine, not an authority to follow: nothing crosses without asking
what it was for, and converted code re-earns its place with tests here.
## What we claim, and to whom
Positioning, recorded because a session that re-derives it tends to describe
the machinery instead of the value:
- **A system of record is where the official version lives.** When the ledger
and a spreadsheet disagree, the ledger wins. Businesses have had them for
decades; **AI never did** — it answers from everything it has ever read,
which is exactly why it cannot tell you which of its sentences were checked.
KSoR is that record, for institutional knowledge.
- **Vendor-free is the ownership argument.** The agent surface speaks MCP, an
open standard: one corpus will answer in any assistant, agent framework, or
worker the owner writes. What a customer owns is the source; runtimes are
interchangeable. Never position ksor as an integration with one assistant.
Since decision 27 the SOURCE is open too, not only the protocol: the record
is an OKF bundle in the KSoR Profile, so `knowledge/` handed to any OKF
consumer reads as a conformant bundle with no ksor in the loop. That is the
strongest available form of this claim — say it that way rather than
reasoning from the protocol alone.
- **The interesting problem is not retrieval.** Chunking, embedding, and
hybrid search are commodity. Whether an agent can be _trusted_ is decided by
the governance of what it reads — provenance, something citable, and a
measured floor under which it declines. Lead with that, not the pipeline.
- **Agents are the operator, not the audience for a manual.** The owner tells
the coding agent they already use; scaffolded projects will therefore ship
skills and rules as a product surface, not documentation.
- **Out of the box the owner is meant to touch knowledge only** — plain
markdown, in any language they write in.
## Vocabulary
Used precisely; do not repurpose.
| Term | Means |
| ------------------- | ------------------------------------------------------------------------------------------------ |
| **corpus** | the governed markdown under `knowledge/` — the source of truth. It is an OKF bundle (27) |
| **concept** | one governed document in the bundle. Its id is its bundle-relative path without `.md` |
| **companion** | a file attached to a concept — summary, flashcards, quiz, slides. Never a concept itself (24) |
| **instance** | one deployment configured (`instance.md`): identity, floors, budgets. **Not governance** |
| **policy** | `.ksor/governance.yaml` — the audience registry and the approval/takedown authorities (27) |
| **ledger** | `.ksor/takedowns.yaml` — the committed, append-only record of every takedown act (27) |
| **build** | one execution of `ksor build`, identified by a `build_id` |
| **refresh** | the scaffold's `pnpm refresh` — PUBLISH: `ksor build`, then `ksor ingest --flip`, then `ksor gc` |
| **build_id** | what every machine artefact stamps — the content hash of everything a projection reads |
| **generation** | the monotonic version of published content — what a citation pins. **Not `build_id`** |
| **build.lock.json** | the committed record of a build: what was published, from which commit, with which toolchain |
| **surface** | something that serves the corpus — the website and the MCP server |
| **scaffold** | what `ksor init` writes into an adopter's repo — owned by the adopter (decision 4) |
| **audience** | an identifier a concept lists and a viewer holds; the concept is admitted when they overlap |
| **viewer** | the audience list a build or a request is made for — always contains `public` |
| **trust tier** | unverified · machine-confirmed · human-reviewed — derived from `verified[]`, never declared |
| **abstain** | the corpus does not cover this — a correct answer, never an error |
One command is confused with its own halves, so the mental model is written
down rather than left to be inferred: **`ksor build` makes the SITE correct**
(it checks the record, regenerates the indexes and writes the lock — no
database), **`ksor ingest` makes the AGENT DOOR correct** (it embeds, loads
Postgres and flips a generation), and **`pnpm refresh` runs both** so every
surface is current. The scaffold's script is the name an adopter uses;
`ksor ingest` is the name CI and this repo's docs use, because there the
individual step is the subject. That split is deliberate — it is not two ways
to do one thing.
Two pairs are confused often enough to be worth naming. **`build_id` is not a
`generation`**: the first connects every projection of one publication, the
second is the kernel's counter a citation pins, and they move independently.
**Audience is not `visibility`**: `visibility` was one ranked tier per document
and is now a refused key — a concept holds a LIST, a viewer holds a LIST, and
membership decides rather than rank (decision 27). "Level", the 0–4 numeric
ladder, is retired with it: what a record meets is the conformance floor, and
what it climbs is the trust tiers (product principle 7).
## Repository layout
| Path | What it is |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `packages/ksor/` | the published package: CLI + SDK (MCP surface lands here) |
| `packages/{postgres,content,gateway-kit,content-gateway}/` | the kernel (decision 11): Postgres access discipline (pooling, scoped transactions, retry classification), the content corpus store + retrieval + abstention, serving postures, and the content MCP door (one gateway per record — `content-gateway` today; `identity-gateway`, `praxis-gateway` follow). BUNDLED into `@panaversity/ksor` — the CLI inlines all four and exposes one `ksor` binary; the kernel packages stay private, never published (decision 12 publish revision 2026-08-20) |
| `packages/ksor/docs/` | user docs, shipped inside the npm tarball |
| `workbench/example-corpus/` | living KSoR fixture: dev target, test + eval surface |
| `docs/status.md` | the only authority on what is implemented (npm links it) |
| `research/` | plans and records; frontmatter is guard-enforced |
| `specs/` | one-page feature contracts; frontmatter is guard-enforced |
| `.agents/skills/` | repo-maintenance skills (`.claude/skills` symlinks here) |
| `scripts/` | guards, corpus checks, boundary tests — plain node/vitest |
| `tsconfig.base.json` | the shared strict base — extend, don't fork |
| `.githooks/` | committed pre-commit hook (`pnpm prepare` sets hooksPath) |
### How a package is named
The table above is generated by a rule, not by taste. ksor is built to hold
MORE records than one — the content SoR today, identity and praxis after it —
so the package names have to say which layer a thing belongs to before anyone
argues about it:
| Layer | Name | Holds |
| --------------------- | ----------------------- | -------------------------------------------------- |
| shared infrastructure | `ksor-postgres` | pooling, scoped transactions, retry classification |
| shared serving | `ksor-gateway-kit` | auth, fail-closed bind, transport security |
| ONE record's store | `ksor-<record>` | `ksor-content` |
| ONE record's MCP door | `ksor-<record>-gateway` | `ksor-content-gateway` |
| the product | `@panaversity/ksor` | the published CLI; bundles the rest |
A second record adds exactly two packages — `ksor-identity` and
`ksor-identity-gateway` — and no new pattern. If a change needs a pattern that
is not here, that is the signal to stop and decide, not to invent a name.
**Never name a package for a layer that admits anything.** `platform`, `core`,
`common`, `shared`, `utils` constrain nothing, so unrelated code accretes into
them — which is exactly what happened to `ksor-platform` before it became
`ksor-postgres` (decision 11 revision 2026-08-20): it had quietly collected env
helpers that duplicated gateway-kit's. The test a name must pass is that
"does this belong in X?" has an answer. `postgres` passes; `content` passes (it
is a record, alongside identity and praxis); `platform` never could.
## Commands
```sh
pnpm install # respects the packageManager pin (pnpm 11)
pnpm build # tsdown per package (<10s)
pnpm typecheck # tsc --noEmit, packages + scripts (<5s)
pnpm lint # oxlint --fix (<1s)
pnpm fmt # oxfmt (<1s)
pnpm guard # guard-invariants.mjs (<1s)
pnpm check:corpus # the SHIPPED docs' frontmatter and links (<1s)
pnpm test:unit # *.test.ts, colocated, pure (<3s)
pnpm build && pnpm test:integration # built artifacts + repo-tree suites (~2 min)
pnpm test:e2e # the KSOR_E2E browser suites, after the playwright install
pnpm publint # package manifest/tarball correctness (needs build)
```
Run fmt/lint/typecheck freely — they are cheap. Treat local checks as advisory:
CI is the source of truth — don't burn cycles making advisory gates pass before
handing off.
## Decisions
Recorded here, in the same change that acts on them; each names what would
reverse it, and a reversed decision keeps its entry with a revision note.
**Work that contradicts one stops and goes back to a human.**
1. **TypeScript and npm are the front door.** The site toolchain must execute
on the adopter's machine, so Node is a prerequisite no other runtime can
hide; a second mandatory runtime buys the adopter nothing. Reversed if the
end user ever stops needing a local Node build.
2. **Package `@panaversity/ksor`, command `ksor`.** Unscoped `ksor` is blocked
by npm's publish-time similarity gate (verified by a real `E403`; a registry
404 is not evidence of publishability). Not reversible.
3. **Apache-2.0, whole repository.** Reversed only by an explicit owner
relicensing decision recorded here.
4. **Corpus scaffolds are copy-into-repo** (the shadcn model, validated by our
own study of its mechanics): the adopter owns what `ksor init` emits;
updates are offered as diffs and applied only by explicit overwrite.
Reversed per-file if a scaffold file must stay framework-owned to preserve
a product guarantee. _Revision 2026-08-25 (decision 27): the update vehicle
this decision promised without naming now exists — `ksor migrate --write`
rewrites the adopter's record into the profile, and `--write-site` offers
the byte-copied rule modules to the adopter-owned `system/site` as diffs.
Ownership is unchanged: migrate prints the diff and changes nothing until
`--write`, and it refuses by name rather than authoring anything the
adopter has to mean (a title, a description, the actor behind a takedown)._
5. **Toolchain** per the `research/base-environment.md` §2 ledger: TS 7 native
(never depend on its compiler API before 7.1 — guard rule 6), Node ≥24,
pnpm exact-pinned, pure ESM, tsdown with `isolatedDeclarations` (explicit
types at every exported boundary, oxc fast path for `.d.ts`), vitest tiers,
oxlint+oxfmt, changesets with npm trusted publishing. Reversed per-pin when
a recorded caveat fires. _Revision 2026-08-18: turbo removed — a task
runner for one package earned nothing; plain `pnpm -r` is the whole of it.
It (or a then-current alternative) returns with the first inter-package
dependency edge — the site/lib conversion PR — judged against the real
task graph, not package count. The `pnpm build` vocabulary is the stable
contract either way; the runner behind it is replaceable machinery._
6. **Predecessor conversion is granted** (owner, 2026-08-18): Apache-2.0
covers the predecessor work end to end, and the owner has granted taking it
— Python included — and converting it to TypeScript. This retires the
copy-grant blocker the handover carried. Conversion is engineering-gated,
not licence-gated: ask what a mechanism was for before carrying it, and
converted code lands with its own tests. Not reversible (a recorded grant).
7. **Product design decisions adopted from the predecessor** under decision 6
— each individually reversible with new evidence, recorded here:
**conversation is the interface** (the human runs `ksor init <name>` once,
then talks to the coding agent they already use; CLI verbs are for the
agent); **serving fails safe** (serve refuses to boot unauthenticated at
all — a local run flags it explicitly and binds loopback; a public bind
additionally fails closed unless auth is configured — "disabled by default"
must never silently become an open server); **the governance level is derived, never declared**
(tools report the level the governance artifacts achieve; no `governance:`
key in `instance.md`); **no empty scaffolded directories** (an empty
directory is an unanswered question in the adopter's repo — directories
appear when the ladder or the work demands them); **the site is preview and
review, not an editor** (the agent writes; the human checks).
_Revision 2026-08-25 (decision 27): two clauses gain a sharper form and
neither is reversed. "The governance level is derived, never declared"
holds, but the NUMERIC ladder it derived is gone — tools report the
conformance floor a record meets and the trust rung its `verified[]`
earns; there is still no `governance:` key to declare. And "the site is
preview and review" becomes load-bearing rather than descriptive: the
preview is now the ONLY surface a `draft` reaches, so the review step is
enforced by what every build excludes rather than by convention._
_Revision 2026-08-20: the serving clause read "local serve binds loopback
with auth off", describing a default the code has never had — `buildAuth`
refuses to boot unless SSO is configured OR `KSOR_AUTH=disabled-local` is
explicit, loopback included (`packages/gateway-kit/src/auth.ts`). The
posture is unchanged and STRONGER than the sentence claimed; the wording is
corrected here and in the three docs that had copied it (both READMEs and
the scaffold's AGENTS.md), which were telling adopters a local `serve` would
come up without the flag it requires._
8. **Scaffold structure: root workspace + system roof** (owner, 2026-08-18).
`ksor init` emits the workspace manifests at the repo root (defaults beat
hiding them behind new algorithms), `knowledge/` at root as the record —
CommonMark only, framework-free forever — and ALL code under `system/`
(site now; gateways/packages as earned; growth inside, never beside). The
root set is closed at birth; full lock record and the closed set:
`research/scaffold-structure.md` + `specs/ksor/init/spec.md`. Reversed
per-clause with evidence, recorded there. _Revision 2026-08-20: the closed
root set gains ONE member, `.env.example`. The served rung needs three
variables and one of them is not guessable — `ksor serve` refuses to boot
unauthenticated, so `KSOR_AUTH=disabled-local` is required for a local run, and
a runbook that omitted it dead-ended at its last step (found live). An empty
directory is an unanswered question; a named, commented example of the
variables a rung needs is an ANSWERED one, and it is the only place those
values can live without being pasted into a shell. `.gitignore` gains
`!.env.example` so the example survives the `.env*` rule that hides real
secrets._ _Revision 2026-08-23: the closed root set gains TWO more,
`Dockerfile` and `.dockerignore`. The reasoning is the `.env.example` one
applied to the surface that IS the product: MCP serving is core (decision 11
revision 2026-08-20), a served record therefore has to reach a host, and
every container runtime asks for exactly these two files. Emitting them is
what makes "vendor-free is the ownership argument" true in the artifact
rather than only in the prose — the Dockerfile names no host, and
`vercel.json` POINTS AT it instead of replacing it, so moving hosts is a
redeploy and not a rewrite. A test asserts that neutrality directly, because
it is cheap to lose to one convenient host-specific line and nothing else
would go red. The same revision extends `vercel.json` from one static build
to two services (site + door) behind one domain. Verified live before it
shipped, and the verification earned its cost: a project-level
`trailingSlash: true` — harmless while the project was static-only —
308-redirected **every door route including `POST /mcp`**, which would have
broken the MCP endpoint of every adopter who deployed. It is removed; the
site's own Next config already sets it where it belongs._ _Revision
2026-08-25 (decision 27): the closed root set gains `.ksor/` — the
Governance Policy and the takedown ledger, both committed — and
`build.lock.json`. The `.env.example` reasoning applies unchanged: these
are answered questions, not empty directories, and the ledger in
particular is why a record with no database can take a document down at
all. `.gitignore` becomes `.ksor/*` with the two files negated, because
the directory form `.ksor/` cannot be negated (verified against git).
"CommonMark only, framework-free forever" gains exactly ONE extension, GFM
footnotes, for the reason decision 27 gives: it is the only extension that
degrades to readable text in a plain renderer, which is what
"framework-free" was protecting._ _Revision 2026-09-01: the closed root set is
recorded as gaining TWO members it had already gained in silence —
`.ksor/people.yaml` (0.0.52 — this revision first said 0.0.53, corrected
2026-09-02 against the tags: `1e60b9d` added the file and its `.gitignore`
negation, and both are present at `@panaversity/ksor@0.0.52` and absent at
`0.0.51`) and `.mcp.json` (0.0.54). Both were emitted and
asserted in `init.integration.test.ts` with no row in the init spec's
emitted-tree contract and no revision here, which is exactly the review step
this decision names as where additions are locked. The omission was not
cosmetic: `.ksor/people.yaml` publishes the approver printed on every
document page and had been left outside `build_id` and outside the site's
staleness gate, so the human and machine surfaces of ONE build could name
different approvers with nothing red — fixed in the same change that records
this. `.mcp.json` ships a second, vendor-operated MCP record into every
adopter repo; whether it should is an OWNER question and is left open here
rather than settled by the file's existence._
9. **Site shell: one in core — Next.js + Fumadocs + shadcn** (owner,
2026-08-18), replacing Docusaurus natively before v1 traffic. No shell
selector at init (one obvious way; a flag forks every skill, test, and
recipe). Choice lives in three existing layers: the pinned **surface
contract** (render the record, llms.txt, per-page md artifacts, browser
smoke, no authored content — the shell is a slot), adopter ownership of
`system/site`, and future registry-distributed alternative shells.
_Revision note: supersedes the site-shell open question and the
primitives proposal §4 stay-Docusaurus lean (proposed, never ratified) —
the extensibility ceiling (auth, features), agent-ecosystem alignment,
and the verified portability of the predecessor's remark layer decided
it._ Reversed if Fumadocs's static export or llms surface regresses
before the site slice ships. _Revision 2026-08-18: the two-shell proof
is in-tree — `workbench/shells/docusaurus/` swaps in by its README
recipe and one shell-agnostic conformance suite runs the surface
contract against both shells in CI. `ksor init` still emits Fumadocs,
always; no selector was added._ _Revision 2026-08-24 (owner): the
second shell is RETIRED — `workbench/shells/docusaurus/` deleted, the
conformance suites run one. Every surface the record grew had to be built
twice to keep it green, for a shell no adopter runs (`ksor init` has always
emitted Fumadocs, no selector). The five-clause surface contract is
unchanged and still asserted against one implementation; both suites keep
their `.each(SHELLS)` shape so adding a shell back needs no restructuring.
Restored by an adopter actually swapping one; the recipe is in git history.
**Consequence to hold onto:** this removes the structural objection to
reversing the `output: "export"` clause, since a Docusaurus shell could
never satisfy a Next-server contract. That reversal is now CHEAPER, not
decided — it still contradicts `specs/ksor/visibility/spec.md`, and site
auth does not authorize it (issue #130)._
10. **Scaffold templates are MIT-0** (owner, 2026-08-18): init's output
lands in the adopter's proprietary repo free of attribution
obligations, and init never emits a LICENSE file into a repo whose
knowledge is theirs. The grant sentence lives in the scaffolded README.
_Revision 2026-08-25 (decision 26): the emitted `check.mjs` will bundle
the `yaml` parser and carry its ISC notice in a banner; the templates
themselves stay MIT-0._
11. **The content kernel converts whole; serve is the graduated rung**
(owner, 2026-08-19). The predecessor kernel's content SoR
(`sor-agentfactory @ b554f91`: sor-content, the sor-platform trim, the
gateway-kit auth/serve/harden slice, and the content gateway) converts
to TypeScript in this workspace — retrieval, generations, the calibrated
abstention method with its measurement history, and the fail-closed
dual-mode serving posture — with the Python suite as conversion oracle:
gold sets, schema contract, and calibration data extract as conformance
fixtures first, red before any port. The MCP door speaks the MCP
TypeScript SDK, stateless Streamable HTTP (one transport — the shape
the production gateway ships; a local agent uses the same URL).
Placement is the
ladder: `ksor init` stays database-free (`pnpm dev` unchanged — the
out-of-the-box claim holds); the kernel lands as framework-owned
workspace packages behind `ksor serve`, and adopters climb to it.
Retrieval runs in a real embedding space through the merged provider
seam; CI carries a Gemini key for the gated live tiers. Record:
`research/kernel-conversion.md`; contract: `specs/ksor/serve/spec.md`.
Each crossing mechanism still answers decision 6's gate individually;
the database-free-init clause is reversed only by an explicit owner
decision recorded here. _Revision 2026-08-20: the kernel's floor package was
renamed `ksor-platform` → **`ksor-postgres`** (directory `packages/postgres`).
"Platform" was inherited from the predecessor and is a bucket name — it
constrains nothing, so unrelated code accretes: it had already collected
`envInt`/`envFloat`, duplicating a differently-shaped pair in gateway-kit.
The package is Postgres access discipline (pooling, scoped transactions with
GUCs, retry classification), and the new name makes membership answerable.
The env helpers moved to `content`, their only consumer. Private package, so
no published contract changed._ _Revision 2026-08-20 (owner): MCP serving is a
CORE surface of every KSoR, not an optional rung, so the served tool ships
as a FIRST-CLASS scaffold dependency — `ksor init` writes
`@panaversity/ksor` into the scaffold's `package.json` dependencies, pinned
to the EXACT CLI version that scaffolded the project (via the existing
`KSOR-STAMP-VERSION` stamp), plus `serve`/`ingest` convenience scripts, so
`pnpm serve` is a local, version-pinned command rather than an `npx`
afterthought. This does NOT reverse the database-free-init clause:
installing the dependency needs no database, and `pnpm dev` still runs the
site without one — the "climb" to serving is now only standing up Postgres
and a provider key, not acquiring the tool. The committed scaffold lockfile
stays site-only (a stamped version cannot be pre-resolved into a committed
lock), so the adopter's FIRST `pnpm install` is non-frozen and writes the
lock; their shipped `validate.yml` runs no install, so their CI is
unaffected. Verified live: an emitted scaffold's `pnpm install` resolves the
pinned dep, links the `ksor` bin, and `pnpm exec ksor` runs. Reversed only
by an explicit owner decision recorded here._
_Revision 2026-08-25 (decision 27): the database-free clause GROWS rather
than narrowing. `ksor build` joins `init` and the site on the free side of
the ladder — it generates the indexes, runs the record checker and writes
`build.lock.json` with no database, no provider key and no network, so the
governance decision now runs for static output at level 0. What moved the
other way is that `ksor ingest` runs the SAME checker and refuses without
a fresh lock: the served rung can no longer publish a record the free rung
would have refused._
12. **The kernel's dependency set** (2026-08-19, with decision 11; each
entry individually reversible by a better tool winning a recorded
comparison). One Postgres driver: `pg`, queried **raw** —
**`schema.sql` stays the DDL source of truth** (converted from the
oracle; a rendered-SQL test in `schema.integration.test.ts` pins it).
_Revision 2026-08-19: `drizzle-orm` was proposed here for typed queries
with an information_schema drift test, then DROPPED as unused before it
landed — no package declares it, and guard rule 5 enforces its absence;
raw `pg` with explicit projection-width guards carries the kernel. If a
typed-query layer returns, it re-earns its place with the drift test the
original proposal named._ _Revision 2026-08-19 (publish prep): `@types/pg`
is a declared `dependency` (not a devDep) of `ksor-postgres`,
`ksor-content`, and `ksor-content-gateway` — their published `.d.mts`
exposes `pg.Pool`/`PoolClient` in the public API, so an external TS
consumer needs the types to resolve; enrolled in guard rule 5's per-package
allowlist._ `zod` from the catalog
pin (the reserved "first validated public API" arrived). `@google/genai`
as the default embedding provider behind the seam — the seam, not the
vendor, is the contract. `jose` for the gateway kit's public-door JWT
verification. `@modelcontextprotocol/server` (SDK v2, the 2026-07-28 revision; `@modelcontextprotocol/client` is a devDep for the acceptance walk) for the MCP surface. Guard
rule 5 now scans every workspace package against this list; install
scripts stay denied (recorded in `pnpm-workspace.yaml`
with verified why-comments). _Revision 2026-08-20 (ONE package, owner):
the kernel is BUNDLED INTO the published CLI — `@panaversity/ksor` inlines
`postgres` + `content` + `gateway-kit` + `content-gateway` (workspace
devDeps, tsdown `noExternal`), carries their external runtime deps (`pg`,
`@google/genai`, `@modelcontextprotocol/server`, `hono`, `@hono/node-server`,
`jose`, `zod`, `@types/pg`), and exposes ONE binary `ksor` with all verbs:
`init`/`dev`/`build` plus `serve` (runs the gateway IN-PROCESS, a direct
import), `ingest`/`schema`/`calibrate`/`gc` (delegated to the bundled
write-plane dispatcher). So an adopter installs ONE thing — `@panaversity/
ksor` — for everything, and the content SoR is always present. This
REVERSES the decision-1/13 zero-runtime-deps guarantee for the CLI (by
owner call, weighed against a separate package): the cost is that
`npx @panaversity/ksor init` now pulls ~60MB; the win is no second package
to publish (the existing `@panaversity/ksor` publish + trusted-publisher
setup covers it) and no spawn/resolve dance. `content`'s `schema.sql` ships
as `ksor/schema/` (build-copied, gitignored, resolved via
`import.meta.url`). `postgres`/`content`/`gateway-kit`/`content-gateway`
stay `private: true` forever — dev/test workspace packages, bundled, never
published. Verified by a real `pnpm pack` → `npm install` in a fresh dir:
`ksor serve`/`ksor schema`/`ksor ingest` run, schema resolves, deps are
self-contained. The prior revision (separate `@panaversity/ksor-content-
gateway` package, serve-by-spawn) is superseded._
_Revision 2026-08-22 (issue #54): `@google/genai` is REMOVED. It was 17 MB
installed and brought 30 transitive packages with it — 54 MB and 52
top-level packages for a `ksor init` that needs neither — to make exactly
two HTTP calls that `providers/gemini.ts` already wrapped behind a
structurally-typed client slice. `lib/providers/gemini-rest.ts` implements
that slice with `fetch`: **22 MB, 22 packages**. The swap was gated on one
measurement taken BEFORE any code was written — SDK and REST return
byte-identical vectors for the same text, model, `outputDimensionality` and
`taskType` (max per-component difference 0.000e+0 at 1536 dims), so no
stored embedding and no calibrated floor moved. Had they differed by a
rounding step this would have silently invalidated `vector_floor` on every
record. The seam is unchanged and still vendor-neutral, so a provider that
prefers an SDK can supply one through `clientFactory`; the live call in
`gemini.live.db.test.ts` remains the drift tripwire and now meets the vendor
without a library in between. Reversed if the vendor's REST contract starts
changing faster than we can follow it, which the live test is what would
tell us._
_Revision 2026-08-25 (decision 26): `yaml` 2.9.0 joins the set — ISC,
zero transitive dependencies, exact-pinned — for the record module; the
CLI carries it because it bundles `content`._
13. **The content gateway's HTTP door composes the SDK's Web-standard
transport, not a hand-rolled one** (owner-directed, 2026-08-19). The MCP
surface IS the product; shipping a door hand-built on `node:http` — which
reimplemented routing, body parsing, security headers, and the loopback
DNS-rebind default that the SDK already gets right, and in which three
security review findings landed — is shipping bad MCP for the one thing
that is the point. The door uses
`WebStandardStreamableHTTPServerTransport` (`Request → Response`,
stateless) behind Hono, with Host validation as middleware (the shape the
SDK's deprecation of its transport-level option points to) and
`bodyLimit` middleware replacing the hand-rolled BODY-SIZE cap
(`secureHeaders` was proposed here for the header hardening and never
adopted, so that half is still hand-written — see the 2026-08-27 revision).
`hono` and `@hono/node-server` are declared runtime deps of
the content-gateway. What stays ours because it is good: `buildAuth` and the
fail-closed boot posture, the three probes, and the whole content kernel.
Reversed only if the SDK drops the Web-standard transport. _Revision
2026-08-20: the transport choice stands unchanged. The packaging
sub-claim that these deps "never reach the published zero-dep `ksor` CLI"
and "the gateway can never fold into it" is SUPERSEDED by decision 12's
2026-08-20 revision: the gateway IS bundled into `@panaversity/ksor`, which
now carries `hono` + `@hono/node-server` (guard rule 5 enrolls them) and is
no longer zero-dep. The SDK's dependency weight is now install weight of the
one published package, not a reason to keep two._ _Revision 2026-08-20 (SDK
v2): upstream split the monolith into `@modelcontextprotocol/server` +
`@modelcontextprotocol/client` 2.0.0 (GA 2026-07-28) implementing the
**2026-07-28** revision, and the gateway moved to it before shipping — this
PR is the MCP surface's first release, so shipping it on a superseded
revision would have made the product's headline surface out of date on day
one. The transport choice STANDS: v2 keeps
`WebStandardStreamableHTTPServerTransport`. What changed is the entry — the
door now composes v2's `createMcpHandler` (per-request server factory,
`legacy: "stateless"`, `responseMode: "json"`) instead of hand-driving a
transport per request, because the modern era is served by that entry and
NOT by a bare transport (proved by probe: the bare wiring answered
`server/discover` "Method not found" and rejected the 2026-07-28 header as
"Unsupported protocol version"). 2025-era clients keep working through the
same stateless idiom, so the upgrade is not a cutoff. v2 also deprecates its
transport-level `allowedHosts`/`enableDnsRebindingProtection` in favour of
external middleware — which is what this door already does. Dependency
weight falls (`server` → `zod` + `core`; the Node middleware is
`@hono/node-server`, already carried) rather than rising._
_Revision 2026-08-27 (issue #151): two sentences above describe a door that
was never built, and one of them was still being repeated in a guard
comment. **`bodyLimit` is real** — imported and applied at
`content-gateway/src/http.ts:26,522` — but **`secureHeaders` was never
adopted**: nothing in the tree imports it, and the door sets its own pair by
hand (`http.ts:330-331`, HSTS + `x-content-type-options: nosniff`, "nothing
else"). The CODE is right and this entry was wrong, so the entry is
corrected rather than the code. **And hono is not free.** "Already the MCP
SDK's own transitive deps, so zero new install bytes" was true of the 1.x
monolith and false from the moment the same revision above moved to v2,
which depends on `zod` and `@modelcontextprotocol/core` and nothing else
(checked against the installed tree, 2026-08-27); `@hono/node-server` is
likewise carried by nothing but the door itself. The reason that survives is
the one this decision already gives — the SDK's only HTTP shape is
Web-standard, and hono is the shape that needs no bridge to it — so the
weight is a cost paid deliberately, not an absence of cost. Guard rule 5's
why-comment carried the same false sentence and is corrected with it._
14. **Takedown denial is scoped — per-node by default, subtree by explicit
opt-in** (owner, 2026-08-19). A review found the ported denial was
per-node only, so a section takedown left its documents served. Rather
than flip the whole mechanism to subtree (a governance reversal), scope
is a property of the takedown row: `scope = 'node'` (default) denies
exactly the listed `stable_id` — identity, immune to reorganization, an
auditable frozen list; `scope = 'subtree'` denies the node AND every
descendant, resolved at SERVING time by a recursive `parent_id` walk.
Serving-time (not write-time expansion) is required because
`takedown_denylist` has no generation column by design — a subtree deny
must also cover descendants a FUTURE re-ingest adds. The walk is by
`parent_id`, NOT a `stable_id` prefix — a frontmatter `sor_id` override
decouples stable_id from the path, so a prefix both leaks sor_id children
and over-matches prefix-siblings (both proved in `takedown.db.test.ts`).
One seam (`lib/takedown.ts`: `DENIED_CTE` + `DENY`) binds search, read,
outline, and the calibration sampler; an empty denylist makes the seed
empty and the recursion terminate at once, so the hot path pays nothing.
Schema: `takedown_denylist.scope` (schema_meta 2.1, additive with a
default → a 2.0 reader still reads a 2.1 DB; 2.2 adds the governance
columns the same additive way). When the `takedown` write
verb lands it must make a container selection an EXPLICIT choice —
expand to leaves (identity) or declare a subtree rule — never silently
guess. Reversed per-clause with evidence; the `node` default is not
reversible without an owner decision (it is the identity guarantee).
_Revision 2026-08-25 (decision 27). Four changes, one of them an owner
decision against the clause above. **`sor_id` is retired** — path is
identity, so a renamed denied document gets a new id and "immune to
reorganization" is weakened knowingly; the compensating control is
`ksor-takedown-dangling`, which refuses the BUILD when an in-force entry
names a concept that no longer exists, so a rename goes red rather than
republishing. **A denial is a ledger entry first and a row second**:
`.ksor/takedowns.yaml` is committed and append-only, the verb writes both
in one act, and a lift is a revocation ENTRY setting `revoked_at` on the
row rather than a deleted line. **An entry may be marked `removed`**, the
sanctioned way to delete a denied file, after which the path reappearing
refuses. **A directory is ALWAYS the `#section` node** — previously only
an index-less one was — which is the anchor a `subtree` entry names. The
`parent_id` walk, the per-node default and the explicit container choice
are untouched._
15. **Governance is stored on the record, not re-derived per surface**
(2026-08-20, from the end-to-end review). The ingest adapter kept four
frontmatter keys and dropped the rest, so `visibility`, the authored
`status`, `owner` and `provenance` existed only in markdown and each
surface implemented its own subset — the site enforced `visibility:` and
the MCP door could not, because the record did not carry it (a document
marked `visibility: internal` was hidden from the website and served in
full to every agent, reproduced live). Schema 2.2 puts them on
`content_nodes`; ONE frontmatter module reads them; `lib/audience.ts` is
the single serving seam, bound the way `lib/takedown.ts` binds denial. A
new guarantee about a document is a COLUMN plus a seam, never a filter in
one surface's build step. Reversed only by an owner decision recorded here.
_Revision 2026-08-25 (decision 27): the rule is unchanged and the row got
much wider. Schema 2.5 carries the audience LIST (`audience TEXT[]`, GIN
indexed, replacing the ranked `visibility`), the authored status on the
new vocabulary, the OKF trust block as JSONB (`sources`, `verified`,
`generated`, `approval`, `deprecated`), `effective_from`/`stale_after`,
and a derived `trust_tier`; the run carries `build_id`, the policy as a
row with its digest, and the ledger's id set. The single seam widened with
it — `lib/admit.ts` composes audience overlap, `lib/lifecycle.ts` and
`lib/trust.ts` into ONE admitted set, bound beside `DENY` in both search
arms, `read`, `outline` and the calibration sampler — so the door reads
lifecycle and trust the way it already read audience: from columns, not
from markdown each surface parsed for itself._
16. **Forward migrations exist and are walked, not sorted** (2026-08-20).
`schema/migrations/<from>-<to>__<slug>.sql`: each file names both ends of
its step, so a missing step refuses instead of being silently skipped, and
each applies in one transaction with the `schema_meta` row recording it.
`schema.sql` remains the DDL source of truth for a FRESH database. This
retires "drop and recreate", which destroyed the two tables that cannot be
rebuilt from markdown. Reversed only with a recorded replacement.
17. **A pool with a floor of ZERO — not a connection per call, not a pinned
set** (owner-directed, 2026-08-21). The question the owner asked is the
right one for a product that will run on Cloud Run against a serverless
Postgres: who holds a connection, and for how long. Three postures were
weighed and the middle one is ours.
_Connect per call_ pays a full handshake on every request. Measured
locally (Postgres 17.7, loopback, no TLS, n=30): a fresh connect + trivial
query is **3.02ms** median against **0.15ms** on an open one — a **2.87ms**
floor under every request that does no work at all. A remote TLS endpoint
is materially worse, because the handshake adds round trips this local
number does not contain. Paying that per request buys nothing an idle
timeout does not already buy.
_A pinned pool_ (`min: 2`, which ksor inherited from the predecessor) is
the posture the owner objected to, and the objection was correct — more so
than it looked. pg-pool reaps an idle connection ONLY while the pool is
above `min` (pg-pool 3.14 `index.js:409`), and it does not open anything
eagerly. So a non-zero `min` does not prewarm: it pins that many sockets
open forever and prewarms nothing. The predecessor's psycopg pool DID
prewarm, which is why 2 was reasonable there; ksor took the number without
the mechanism and got the cost with none of the benefit. Against a compute
that suspends on idle, those pinned sockets are also the ones most likely
to be dead on the next request.
**What ships: `min: 0` with a 10-second idle timeout.** A server that is
quiet for ten seconds holds NOTHING — no socket, no backend, nothing for a
suspend to kill and nothing billed on a per-connection plan — which is the
"connections are closed" property, obtained by expiry rather than by
per-request teardown. Inside a burst, connections are reused and the
handshake is paid once. `prewarmPool` exists for the deployment that wants
warm sockets and asks for them explicitly; it is never implied by `min`.
This posture is only safe because the reconnect path is real, so it is
part of the decision: `withGuardedClient` keeps an error listener attached
for the whole checkout (pg-pool removes the client's own during one, and a
socket dying mid-statement then reaches Node as an uncaught exception and
exits the process), and `acquire` distinguishes a saturated pool from a
slow connect so a cold reconnect is retried rather than reported as
exhaustion. Held by `idle.db.test.ts`, `checkout-error.db.test.ts`, and an
MCP-client suspend/resume test that terminates every backend and asserts
none survived before the next call answers.
Walked live 2026-08-21 against a served record: Postgres stopped under the
running gateway → the in-flight request returned "content store temporarily
unavailable" and the process stayed up; Postgres restarted → the FIRST
request answered with cited hits; SIGTERM → drained in 0.34s with the port
released and no orphan.
Reversed if a deployment target makes per-request connection genuinely
cheaper (a local pooler sidecar would), or if a measurement here shows the
idle window costing more than it saves.
_Revision 2026-08-21: the posture the reversal clause names is now an
OPT-IN rather than a fork — `KSOR_DB_CONNECT_PER_REQUEST=1` releases every
connection with destroy, so each call opens and closes its own. The default
is unchanged and unchanged for the same reason: measured on loopback,
per-request costs **2.58ms/call** against **0.13ms** pooled, and a remote
TLS endpoint widens that gap rather than narrowing it. What the option
buys is not a property the default lacks — a quiet server already holds
ZERO — it is the deployment where a pool is a fiction: an external pooler
sidecar, or a runtime that reuses no process between invocations. Both
postures are asserted in `connect-per-request.db.test.ts`, including the
measurement, so the default stays a choice rather than a habit._
18. **One rule, two surfaces, one table** (2026-08-21, from the visibility
leak's fourth recurrence). The site and the kernel enforce the SAME
visibility rule in two languages — TypeScript in the site's build, SQL in
the serving predicate — and it drifted four separate times while each
side's own tests stayed green, because each side was internally consistent
with itself. So the rule stops living in two heads: `AUDIENCE_CASES`
(`packages/content/src/lib/audience-conformance.ts`) IS the rule, as a
decision table; the SQL predicate is asserted against every row through
real Postgres, and the TypeScript half against the same rows.
The TypeScript rule itself is ONE canonical file
(`packages/content/src/lib/audience-rule.ts`), copied byte-identically into
the scaffold — the site cannot import the kernel, whose package carries pg
and the embedding providers, so the copy is asserted by a drift test rather
than trusted. A surface that drifts now fails on the ROW it broke.
Extends to any guarantee two surfaces must both honour; the next one is
takedown, which is already single-seam on the serving side. Reversed if the
site ever can import the rule directly, which would make the table a
convenience rather than a guard. _Revision 2026-08-25 (decision 26): the
OKF-native record makes the copied rule modules GENERATED at
package-build time rather than hand-kept; until that build entry lands
the byte-copy and its drift test stand unchanged._
19. **A surface that refuses must refuse on BOTH surfaces** (2026-08-21, from
the governance review). Product principle 2 says the site and the MCP door
render the same corpus; the sharper form is that they must also REFUSE the
same corpus. Two states had the site stopping by name while the door came
up clean and served the restricted half: a generation built before
governance reached the node row (schema 2.2 added `visibility` and a
migration cannot backfill frontmatter, so every carried-forward node reads
as the widest tier), and a document declaring `visibility:` in a record
that declares no `audiences:` (an author restricted something and nothing
enforced it). Both are now boot checks in `assertGovernanceServable`, and
schema 2.4 stamps each generation with the schema it was built against so
the first is detectable at all. When a new refusal lands on either surface,
the question to answer is what the OTHER surface does in that state.
Reversed only by an owner decision recorded here. _Revision 2026-08-25
(decision 27): the shared refusal is now a TABLE rather than a pair of
boot checks — record spec §2.5 says what each status is admitted to on
human and on machine surfaces, both surfaces read it, and
`LIFECYCLE_CASES` asserts it through real Postgres and against the site's
copy the way `AUDIENCE_CASES` does. The boot gate grew with it: a denylist
row no ledger entry accounts for refuses, one whose entry was never merged
is reported, and a floor calibrated under a different serving predicate
boots into the declared-but-uncalibrated refusal instead of quietly
reading as `gate: off` — which would have made a refusing record answer
everything, the exact inversion this decision exists to catch._
20. **The keyword arm stays in Postgres — never reimplemented in JS**
(2026-08-21, from an adversarial review of the artifact rung). A
database-free serving rung is more feasible than it looks: exact int8
cosine in plain JS measures 19ms at 5,000 chunks and beats HNSW on RECALL
because it is exact rather than approximate, the abstention gate reads only
the VECTOR arm, and the denial seam already exports JSON. The blocker is
not the vector arm — it is `websearch_to_tsquery`. Its parsing, stemming
and stop-word behaviour diverge from any reimplementation SILENTLY: no
error, just a different set of matches. And this record's own gold shows
how little room that leaves — in-corpus 0.730 / 0.671 against a
scope-adjacent near-miss at 0.683 (`behavioural.db.test.ts`), so the
abstention decision turns on about one hundredth. A tokenizer that stems
one word differently moves which questions get answered, and nothing goes
red. Reversed only by a measurement showing a JS implementation agreeing
with Postgres across the gold set — which is a bigger project than the arm
it would replace.
21. **A governance act NAMES its actor; the tool never guesses one**
(2026-08-21, same review). `--actor` fell back to `$USER` / `$USERNAME` /
`"operator"`, so a ledger row read `runner` under CI and `root` in a
container: a self-asserted string wearing a schema, indistinguishable from
a person who was never there. `retrieval_log.actor` is `NOT NULL` with the
comment "NO default: unset errors loudly" — and the fallback is precisely
what stopped it erroring. `ksor takedown` now REFUSES a denial or a
revocation without `--actor`, before the DSN is even resolved (a missing
actor is an argument error, not an environment one). Read-only modes need
nothing. This is product principle "honest absence, never silent weakness"
applied to attribution, and it generalises: a column that records WHO must
never be populated from ambient state. Reversed only by an identity source
the tool can VERIFY rather than read — a bearer token's subject qualifies,
an environment variable never will. _Revision 2026-08-25 (decision 27):
the requirement extends from the VERB to the FILE. A takedown is now a
committed YAML entry, which anyone with write access can append by hand,
so every entry's actor — denial, revocation, amendment — is validated
against the policy's `takedown_authorities` by `pnpm check`, `ksor build`
AND ingest, not only by the verb. It also names what this decision asked
for and did not get: a policy allowlist is AUTHORISATION, not
verification. Every envelope says `checked: policy` for exactly that
reason, and change-control verification against repository history is
what would let it say otherwise._ _Revision 2026-09-02 (KSP R23): the
first verification tooth. `ksor-generated-stale` verifies the
`generated.at` instant against repository history — a `stable` body that
differs from a committed version stable under a stamp this tree has not
ADVANCED past (the same instant, or one moved backward) refuses at
`ksor build` and `ksor ingest` (not at the emitted `check.mjs`, which stays
the format gate), and where history is unreadable each says
`change-control: not checked` rather than passing.
It verifies WHAT changed and WHEN it was stamped, not WHO: R22 and R25
still await an identity source the tool can verify, and
`approval.checked` stays `"policy"` — the flip is a public-envelope change
and is not made as a side effect of this._
22. **Navigation is a SHAPE, not a length** (2026-08-22, issue #55 — the first
DELIBERATE divergence from the converted oracle). `classify()` labelled any
segment under 250 code points `nav`, and the serving predicate admits only
`prose`, so a record could be fully ingested and unable to answer questions
it plainly contained. On the curriculum corpus the oracle was tuned against
the proxy holds — a short segment there really is a link list. On a handbook
it inverts, because a handbook's most valuable statements are its shortest.
Walked live on 0.0.14: three ordinary policy statements, three of four
chunks unsearchable, and "how long does a buyer have to send something back"
answered with the scaffold's placeholder against a record stating thirty
days.
A segment is now `nav` when link lines are most of it, or when what remains
after them is under `MIN_CONTENT_CHARS` — the SAME floor the serving
predicate applies, so this never labels `prose` something search would
refuse anyway. Length is not consulted: a 180-character link list is nav and
a 51-character fact is prose, which is the ordering length got backwards.
Measured on the handbook gold, real Gemini embeddings, paired: short
substantive facts **0/9 → 9/9 at rank 1**, the long-prose control held at
**4/4**, and the link-list negative was returned **0** times — so the gain
is correctness rather than permissiveness, which is the distinction the gold
was built to make. Recorded in `evals/baseline.ts`; the harness prints
current against it and the floors may not fall silently.
`CHUNK_POLICY` moves v5 → v6 because it is persisted provenance and the
behaviour it labels changed. The oracle fixture is NOT regenerated — it
stays the record that the port was faithful — and the divergence is asserted
as a property instead: sourceType may differ only `nav` → `prose`, only
where the whole section carries real prose, and everything else stays
byte-identical. That corpus cannot settle the question either way; it
contains no markdown links at all, which is asserted so the next reader does
not mistake its 61 `nav` labels for evidence about navigation.
Adopters get it by re-running `ksor ingest`: chunks are re-classified on
every build, and carry-forward sets only the embedding fields
(`ingest/generation.ts:174`), so unchanged content is not re-embedded.
Reversed only by a measurement showing the shape rule admitting navigation
the length rule kept out.
23. **The tool surface is adopter-owned CODE; the guarantees under it are
verified, not prevented** (owner-directed, 2026-08-23). Agents are the
operator, and an agent pays for a record's tool surface out of its context
window — twice. Measured on the live 81-document book: the three tool
definitions cost **~2,990 tokens, always resident**, and one `search` at the
default `k=10` costs **~3,541 tokens per call**. A record could change none
of it.
`ksor init` emits the REGISTRATION — ordinary `registerTool` with ordinary
zod — into `system/gateways/content.ts`. A config API (`defineGateway`) was
built first and discarded: models are trained on the MCP SDK and on zod, not
on our field names, and a config schema can only ever expose what we thought
of, while `registerTool` lets a record add its own tools.
What stays in the package: the handlers, the output schemas, and the FLOOR
text. Handlers because they are the only thing that can prove a passage came
from the governed record — a hand-written one returning fabricated hits with
plausible `stable_id`s passes every shape check there is.
**The exchange is prevention for verification.** A description is now a
template literal in adopter code, so nothing structural stops someone
dropping the floor. The door therefore inspects its OWN served surface at
boot — in-memory transport, full MCP handshake, `tools/list` — and refuses
`ksor-gateway-floor-missing`. That is this codebase's posture everywhere
else (`assertGovernanceServable`, decision 19, decision 18's table): hand the
code over, then refuse to boot on a state that breaks it. Verified live: a
registration that dropped `FLOOR.search` exited 1 naming the tool and the fix.
Two copies of the registration exist — canonical in the package, emitted in
the scaffold, differing only in the import specifier and pinned by a drift
test. That is FORCED, not chosen: Node refuses to type-strip any `.ts` under
`node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`, verified
directly), so the package cannot import its own emitted template as the
fallback for a deleted file.
`@panaversity/ksor/gateway` re-exports `z` and `McpServer` so a registration
stays a FILE — no package, no build step, no dependency the scaffold must
declare — and so the SDK validates with the same zod instance it was built
against.
Costs recorded rather than argued away. **Renaming tools trades away
cross-record familiarity** (working rule 8); the owner weighed it and chose
renaming, because disambiguating several attached records is the commoner
problem. **A public subpath export is a real pre-1.0 API commitment** and is
the reversible half of this decision. **A tool an adopter adds carries no
ksor provenance claim**, and making that visible to an agent is left open.
Two defects found while building this, both of the same shape — framework
text retyped instead of moved — and both now guarded: a dropped
injection-defence paragraph in the outline floor (`FLOOR_GUARANTEES`), and a
retyped `READ_OUTPUT` serving `content` where the record serves `text`
(`served-surface.golden.json`). Neither was caught by typecheck, unit tests
or the build; both were caught by comparing against what the door actually
serves. Reversed only by an owner decision recorded here.
_Revision 2026-08-25: the two measurements above are SUPERSEDED, and are
kept with their date because the ratio is the point. The OKF-native door
grew two governance surfaces the 2026-08-23 figures predate: every search
hit and every `read` reply now carries the record's stored `governance`
block, and `search` takes a `min_trust_tier` parameter. Measured exactly,
from the served `tools/list` capture rather than an estimate: the three
definitions are **17,394 chars ≈ 4,349 tokens, always resident** (was
~2,990) as transmitted — `search` 8,152 + `outline` 3,552 + `read` 5,686 =
17,390, plus the four characters the `tools` array itself carries; a search hit
carries ~262 chars more than it did, so a `k=10` call is correspondingly
dearer. The per-call figure is NOT re-measured here — the live 81-document
book it was taken against does not exist in this tree, and an estimate
dressed as a measurement is what this entry exists to prevent. The costs
are recorded, not argued away, which is what the decision asks: the
resident surface is the price of governance an agent can read, and `k` is
still the lever (`packages/ksor/docs/tool-surface.md`)._
_Revision 2026-08-25 (decision 27): the exchange holds — the registration
is still adopter-owned code, the door still inspects its own served
surface at boot — but a governance parameter had to reach a file the
adopter may have scaffolded months earlier. `min_trust_tier`'s DEFAULT and
its enforcement therefore live in the HANDLER, not in the registration: a
registration written before the parameter existed keeps working, and the
boot inspection NOTICES its absence, naming the tool and the line to
paste, instead of refusing to boot. That is the shape a later parameter
should take — refuse on a missing floor, notice a missing affordance._
24. **Study attachments are part of their parent, and the collection is where
that is enforced** (2026-08-23, porting the predecessor's summaries and
flashcards under decision 6). A document may carry `<doc>.summary.md` and
`<doc>.flashcards.yaml`. An attachment has no route, no sidebar row, no
`llms.txt` line, no markdown twin, no search entry, no stable id and no MCP
node; it inherits its parent's tier and its parent's takedown entirely.
**One exclusion, not six.** The route table, the sidebar, `llms.txt`,
`llms-full.txt`, `/md/`, the search index and the caveat map all read
`source`, and `source` is built from ONE collection — so `files:
["**/*.md", "**/*.mdx", "!**/*.summary.md", "!**/*.summary.mdx"]` on that
collection is the whole of "an attachment is not a document". Subtracting
per surface is the failure mode `research/visibility.md` §4–§5 names, and
pruning the page tree is not even sufficient: `getSortedPages()`
deliberately re-adds what the tree dropped and the search index never reads
the tree. The existing `meta` collection is pinned to `meta.{json,yaml}` in
the same change — verified against the real record, its default glob
(`**/*.{yaml,json}`) swallows every deck.
**The site-only scope was breached by exactly one predicate, deliberately.**
`isDoc` (`packages/content/src/ingest/adapters/plain-tree.ts`) is a bare
suffix test, so `x.summary.md` ALREADY ingested as a node with
`stable_id: knowledge/x.summary` — one cause wearing four costumes: the
door served a summary the site hides; served an internal parent's summary
at the record default tier; served a taken-down parent's summary undenied
(per-node denial matches a different id, and the subtree walk goes through
`parent_id`, which is the enclosing SECTION, not the sibling document); and
served an orphan the site refuses. Leaving ingest alone was not the
conservative option, it was shipping the leak — critical rule 1.
**Refusals, not defaults.** An attachment with no parent is refused
(`ksor-attachment-orphan`), and an attachment declaring frontmatter is
refused as a CLASS (`ksor-attachment-frontmatter`) rather than by
allow-listing keys — one rule closes `visibility:` widening, `sor_id:`
takedown escape and `status:`/`owner:` claiming governance a non-node
cannot carry. Both live in the build as well as in `pnpm check`, because
staging never depends on the checker having run.
**Two predecessor mechanisms were NOT carried, both defects.** Its spaced
repetition is write-only — `useFSRS` computes a due queue its deck never
reads, rendering `deck.cards` in authored order, so the scheduling
influenced nothing a learner saw. And its deck-version reset does not
exist: it logs, while the toast it raises says progress "was reset due to a
deck update" and fires only from the `JSON.parse` catch, i.e. storage
corruption. Here the queue drives the session, and a card is identified by
a hash of its own text so an edited card resets alone and the notice is
true.
**The scheduler names itself honestly.** `ksor-sm2-v1`, a two-grade SM-2
variant, persisted with the state. Not FSRS (`ts-fsrs` was weighed at
684 KB and zero transitive deps, and declined by the owner), no retention
target claimed, and what it gives up is recorded beside the code.
Contract: `specs/ksor/study-attachments/spec.md`. Reversed per-clause with
evidence; the no-independent-id clause is not reversible without an owner
decision, because it IS the governance guarantee.
_Revision 2026-08-23: a THIRD kind, `<doc>.quiz.yaml`, on the same rule and
with no new pattern — which is the test this decision was meant to face.
Adding it touched the suffix list and nothing else about the guarantee: no
route, no stable id, no MCP node, parent's tier and takedown, all
inherited. It also settles issue #35's open question — "does an agent get
the answers? Should it?" — without a mechanism, because ingest creates no
node and there is therefore no row for `search` or `read` to return. And it
routes around that issue's stated blocker rather than resolving it: a quiz
is a FILE named after its document, not a `:::quiz` directive inside one,
so `knowledge/` stays CommonMark with no grammar ratified. The directive
grammar remains unratified and remains worth ratifying for other reasons.
Contract: `specs/ksor/quiz/spec.md`._
_One thing IS new, and is the reason this revision is recorded rather than
silent: the quiz carries the predecessor's hygiene audit, converted as a
REFUSAL instead of a script. Its own README lists these as bugs that
shipped and were caught by students — every correct answer at one position
across 9 quizzes and 451 questions — and its findings file, six weeks old,
still reports 88% pick-longest in a file nobody fixed. That is what an
advisory checker is worth, so here the checks run inside the schema and a
quiz that fails them cannot be loaded at all. Thresholds diverge
deliberately (60% floors, not its 15–35% distribution target, and no ratio
rule below five questions) because a small bank cannot satisfy a
distribution without the checker choosing an author's answers for them —
which would be governance overreaching into content. Reversed by a
measurement showing the floors refusing honest quizzes._
_Revision 2026-08-24: a FOURTH and FIFTH kind — `<doc>.teaching.yaml` was
built and then REMOVED (the owner's "teaching aid" meant the slide deck,
not a pedagogy panel; it is recorded here because the removal is the
decision, not an accident), and `<doc>.slides.yaml` ships. Again the
suffix list changed and the guarantee did not, which is now three
consecutive kinds added without a new pattern.
What IS new is that a presentation has two possible sources, and the
default is the one the record OWNS: `deck:` carries the slides and the site
renders them. An embedded deck cannot be governed at all — it is not
reviewed in the pull request, not versioned with its document, not
withdrawn when the document is withdrawn, and it can rot to a dead link
with nothing going red. So the linked mode exists for an adopter whose deck
already lives elsewhere, and `ksor-slides-two-sources` refuses a file
declaring both, because two presentations with nothing saying which governs
is precisely the disagreement this product exists to settle.
The linked frame is CLICK-TO-LOAD rather than always-on, which the
zero-external-requests browser assertion forced and which is better than
what it replaced: the site keeps working offline and behind a firewall, and
a reader who only wanted the policy never announces that to a slide host.
Contract: `specs/ksor/slides/spec.md`. Reversed per-clause with evidence._
_Revision 2026-08-25 (decision 27): the class refusal becomes a one-key
ALLOW-LIST for one kind. `<doc>.summary.md` must now carry exactly
`type: Summary` and nothing else — the profile needs a marker to tell a
companion from a concept, and an allow-list of one closes precisely the
leaks the class refusal closed (`visibility:` widening, takedown escape,
governance claimed by something that is not a node) while admitting the
marker. The other four kinds are unchanged: `.yaml` companions are
invisible to OKF and declare nothing. The `index.summary.md` row retires
with the authored index — `index.md` is generated, creates no node, and
cannot carry a summary of its own. The no-independent-id clause, which is
the governance guarantee and the owner-only one, is untouched._
25. **The scaffold meets the adopter's package manager** (owner, 2026-08-24,
issue #28). Decision 1 makes Node the one prerequisite; requiring a
SPECIFIC manager on top re-added the second-prerequisite tax that decision
exists to avoid. `ksor init` now reads `npm_config_user_agent` and emits
the invoking manager's scaffold — npm and bun alongside pnpm, each shape
proven end to end (install, bin resolution, checker, full static build)
before this landed and walked per-manager in CI. Unrecognized or absent
falls back to pnpm, the most-protected posture. Selection is detection
only — no `--pm` flag; the run that scaffolds is the run that knows the
toolchain, and a wrong guess is re-run with the other runner. The pnpm
scaffold is unchanged. npm and bun scaffolds declare `workspaces` in the
manifest, ship no lockfile (the stamped CLI version cannot be pre-resolved
into one — the tarball hash does not exist at template-build time; the
README says to COMMIT the lock the first install writes), and carry the
install-script denial (`.npmrc ignore-scripts=true` for npm; bun's own
default refusal for bun). What neither can carry is pnpm's 48-hour release
quarantine (`minimumReleaseAge`) — that absence is DISCLOSED in the
emitted scaffold (owner chose disclosure over refusing the managers):
honest absence, never silent weakness. This repo's own workspace stays
pnpm (decision 5, untouched). Reversed per-manager if a manager's walk
cannot be kept green; the disclosure clause is not reversible without an
owner decision, because silence about a weaker posture is the failure mode
it exists to prevent.
26. **The record is real YAML, read by one parser** (2026-08-25, with the
OKF-native record — `research/okf-native.md` §2 item 8, `specs/ksor/record/spec.md`).
The kernel and the scaffold read frontmatter with five hand-written line
scanners (`plain-tree.ts`, the site's `governance.ts` and two rule
modules, the emitted `check.mjs`), each a different subset of YAML, and
the profile's nested `ksor:` block, `.ksor/governance.yaml` and
`.ksor/takedowns.yaml` are shapes none of them can read — a scanner that
fails on a nested key fails SILENTLY, which is the visibility leak's
second door (decision 18). `packages/content/src/record/` reads all three
with `yaml` **2.9.0** (ISC, ZERO transitive dependencies, 796 KB of
`dist/` installed; published 2026-05-11, so the catalog's 48-hour
quarantine never holds it), pinned EXACTLY in the catalog, enrolled in
guard rule 5 for `ksor-content` and — because the CLI bundles the kernel
— for `@panaversity/ksor`. What the parser is allowed to hand back is
narrowed at the boundary (`record/frontmatter.ts`): the core schema, one
document, unique keys, plain data only (a `!!binary` Buffer, a
`!!timestamp` Date or an unknown tag is refused, never passed on), and
the fence found by a real-newline walk rather than a multiline regex,
because JS `^`/`$` break on U+2028 where YAML 1.2 does not (found in
review). Reversed if a scanner is shown to read every profile shape the
spec names, which would make the dependency weight buy nothing.
_This revises three decisions in place: decision 10 (the emitted checker
will carry the parser's ISC notice once `check.mjs` is built from the
kernel's rules, plan §2 item 8 — the templates stay MIT-0, the bundled
parser keeps its own licence), decision 12 (the dependency list gains
`yaml`), and decision 18 (the scaffold's copy of a rule becomes generated
at package-build time, not hand-kept; the drift test stays until it is)._
27. **The record is Markdown in the KSoR Profile of OKF** (2026-08-25; plan
`research/okf-native.md`, contracts `specs/ksor/record/spec.md` and
`specs/ksor/build/spec.md`). The README already told the public that a
KSoR record IS an OKF bundle constrained by the KSoR Profile (KSP-001
§4). A fact-map of the tree on 2026-08-24 found the code agreeing on no
axis: five hand-written frontmatter scanners, not one of which could read
the profile's nested `ksor:` block; no serving path that read a
document's status at all, so a draft was searched and read exactly like
an approved one; a takedown ledger that lived only in Postgres and was
EXPORTED to a gitignored file the site read, which is the direction OKF
§4.1.4 forbids; and machine artefacts carrying no build id, no commit and
no tool version. Decision 26 gave the record one parser. This gives it
one grammar, and gives both surfaces one decision to make about a
document. What was decided, each clause reversible on its own except
where marked:
**The conformance floor replaces the numeric ladder.** Level 0 was
`title` + `status`. The floor is `type`, `title`, `description`,
`status`, `ksor.audience`, and a policy naming approval and takedown
actors. The escape for a record that wants neither owners nor sources is
a NON-RESERVED type: the profile names one, `Document`, and promises
never to reserve it, so the type-keyed rules never fire on a project that
has not asked for them. Nothing is demanded of a level-0 record beyond
saying who its documents are for and who may approve them — both of which
it has to know anyway to publish anything at all. Governance is still a
ladder (principle 7); what "the ladder" NAMES is now the trust rungs —
unverified, machine-confirmed, human-reviewed — derived from `verified[]`
and never declared. Reversed if a real adopter cannot reach the floor.
**Audience is a required list, matched by overlap, and omission is
refused.** A concept holds a list of identifiers; a viewer holds a list
that must contain `public`; the concept is admitted when the two overlap.
Rank moves to the viewer and membership stays on the document, which is
what lets every row of `AUDIENCE_CASES` keep its meaning while the
document stops carrying one ordered tier. `KSOR_AUDIENCE` becomes a comma
list (`ksor-viewer-omits-public`, `ksor-viewer-unregistered`); the
registry lives in the policy; a document that declares no audience is
refused rather than defaulted, because the visibility leak recurred FOUR
times while the default lived in someone's head (decision 18). This
reverses the visibility spec's "one value, never a list — set
intersection is where access-control bugs live", and it is reversed with
the evidence that sentence asked for: the decision table now asserts
overlap through real Postgres and against the site's copy, so a wrong
intersection fails on the row it broke rather than in production.
**Owner-only:** this clause is the leak guarantee and is not reversible
without an owner decision recorded here.
**Drafts live in the preview; every other status is admitted per surface
by ONE table.** `pnpm dev` is the review surface (decision 7) and marks
them; every build excludes them from every surface — pages, sidebar,
search index, `llms.txt`, twins, the door — because a static site's
sidebar and search index are machine artefacts too. `KSOR_DRAFTS=show`
admits them to human surfaces only, is recorded in the lock and in
`build_id`, and marks the build `noindex`. A stable concept before its
`effective_from`, one past its `stale_after`, and a deprecated one each
render for people with a badge and stay off every machine surface. The
table is record spec §2.5 and it is the whole rule, so both surfaces
refuse the same states (decision 19) instead of each deciding for itself.
**`index.md` is generated, committed, drift-checked, and never copied
into a stage.** It carries no frontmatter, so it can carry no governance,
so anything authored there would be ungoverned knowledge on a served
surface. Section prose becomes an ordinary concept in the folder
(`overview.md`). The COMMITTED index is the record's own map and lists
every status and every audience — anyone with the repository has the
files anyway — and every projection REGENERATES its index from the tree
it was filtered to, which is the clause that matters: copying the
committed index into a public stage would have published every internal
title as a folder page, the exact leak the visibility work exists to
prevent. Reversed to export-only if committed generated files prove a
review burden.
**`x.summary.md` carries exactly `type: Summary`, and nothing else.**
Decision 24 refused a companion's frontmatter as a CLASS; the profile
needs a marker, so the class refusal becomes a one-key allow-list that
closes the same three leaks — `visibility:` widening, takedown escape,
and governance claimed by something that is not a node. `Summary` is a
companion marker outside the concept type system, not a reserved type;
ingest still creates no node; the widening rule evaluates a companion's
body with its parent's audience. Decision 24's no-independent-id clause
is untouched, and under bare OKF a summary reads as a concept — the
no-id guarantee is a PROFILE rule and KSP-001 draft 10 says so.
**`instance.md` is a profile-shaped document beside the bundle; authority
lives in `.ksor/governance.yaml`.** `format: 2`, with `name` (the one
sanctioned identity key), `title`, `description`,
`toolchain: { requires, scaffolded }` and the deployment keys — no
`status` and no audience, because identity is not knowledge and the
lifecycle table does not apply to it. The bundle root is `knowledge/`, so
a bare OKF consumer handed that directory sees a conformant bundle and
nothing of the site. `audiences:` and `default_visibility:` LEAVE the
instance: two homes for the audience registry is decision 18's failure
mode with a different filename. The policy and the ledger are INGESTED —
registry, authority sets, entry ids, digests — so the door binds to rows
rather than to files a served container does not carry.
**Takedown is an append-only committed ledger that the verb also applies
immediately.** File first, row second, in one act. A revocation is a new
entry naming the one it revokes, never a deleted line; deleting a denied
file is an amendment plus the deletion in one change; every entry's actor
is validated against `takedown_authorities` by the checker, the build AND
ingest, not only by the verb, because a committed YAML file is something
anyone with write access can append to. A ledger that SHRANK against its
own git history or the committed lock refuses the build. What this buys
that the database never could: a level-0 record with no Postgres gets
takedown for the first time, and the site reads the denial from the
repository instead of from an exported artefact. The window between the
verb and the merge is the pull request's review time, disclosed — the
door refuses at once, the site follows the merged ledger at its next
build, which is the latency it already had.
**Two verbs: `ksor build` and `ksor migrate`.** `build` is database-free,
generates every index in memory, runs the checker, and only then writes —
the indexes whose bytes changed, and `build.lock.json`. It is the one
place the governance decision runs for static output, which is what stops
the site and the door reading different truths without a database in the
loop. `migrate --write` is the update vehicle decision 4 promised: it
rewrites a pre-profile record and refuses, by name, everything it cannot
know — a title, a description, a `generated.at`, the actor behind an
existing takedown. Two identities are named apart and never confused in
prose: `build_id` is what the machine artefacts stamp and what connects
every projection of one publication; `generation` remains the kernel's
monotonic counter a citation pins.
**Every timestamp is an ISO 8601 instant with an explicit offset.**
Upstream OKF made the same move under the unchanged `0.2` label, so the
pin is to a commit rather than to a version string, vendored byte-exact
at `specs/ksor/record/okf-SPEC.md` and asserted against the digest every
lock stamps — a pin that names bytes the tree does not hold is not a pin.
A bare `YYYY-MM-DD` is refused (`ksor-instant-form`); `ksor migrate`
widens one to midnight UTC. The cost is that a date is now longer to
type; what it buys is that an embargo, a review deadline and an approval
can be compared across time zones without a convention nobody wrote down.
**`sor_id` is retired — path is identity, everywhere.** **Owner-only:**
this runs against decision 14's node-scope clause, which was recorded as
"immune to reorganization" and not reversible without an owner decision.
It is weakened knowingly: with path as identity, renaming a denied
document gives it a new id. The compensating control is
`ksor-takedown-dangling` — an in-force ledger entry naming a concept that
no longer exists refuses the BUILD — so a rename goes red on both
surfaces instead of quietly republishing, which is the failure the 0.0.18
attack found and the reason the clause existed. `ksor migrate` refuses a
document carrying `sor_id` rather than dropping it, because dropping it
silently retires an identity that takedowns and citations are keyed on.
**`stable` needs approval and NOT verification** — a deliberate
divergence from KSP 4.2.2.3, which requires `verified` on every stable
concept. Coupling them manufactures the event R17 forbids deriving from
approval: an author with an approver and no reviewer would simply write a
`verified` entry, and the tier that exists to say "nobody has checked
this" would never appear. A stable, approved, unverified concept is the
honest state, and it is exactly what tier _unverified_ is for. It is also
what lets the emitted starter ship approved without claiming a review
(the 2026-08-25 revision below). The correction is carried in KSP-001
draft 10 rather than worked around locally.
**GFM footnotes are the one extension to CommonMark.** Reference and
definition, the grammar OKF's per-claim citation uses, with the label
matched against `sources[].id` in both directions. It is the one
extension because it is the only one that DEGRADES: a footnote read by a
pure CommonMark renderer is still readable text, where a directive is
literal colons. This revises decision 8's "CommonMark only" and settles
nothing about the directive grammar, which remains unratified.
**What it cost, recorded rather than argued away.** Day one publishes
nothing until a human approves — one conversational turn, and the claim
made visible. _Reversed 2026-08-25 — see the revision below._ Every
adopter with a numeric floor re-measures it, because the serving
predicate changed and a floor measured under another predicate is a
declared-but-uncalibrated floor; until they do, the door refuses every
search as uncalibrated, which is the invariant rather than a regression.
An upgraded served record has an outage window between
`ksor schema --apply` and the first 2.5 ingest. `approved` becomes
`draft` on migration unless the human approves in the same act. Whether
an edit bumped `generated.at` is UNVERIFIED until change-control
verification lands — the checker compares two authored instants and no
more, and every envelope says `checked: policy` rather than implying
otherwise. _(Verified since 2026-09-02 for the STAMP, not the approver:
`ksor-generated-stale`, decision 21's revision of that date; the envelope
still says `checked: policy`.)_ Actor ids are published with the content, exactly as a commit
author is in a public repository. Stale documents leave the open web at
the next build, so a record with `stale_after` dates needs a scheduled
rebuild.
_Revision 2026-08-25 (owner): **the emitted starter PUBLISHES on the
first build.** What is reversed is the cost clause "day one publishes
nothing until a human approves" and the sentence that read "five starter
drafts, one human approval, five stable documents"; both are corrected
above. The five sample documents now ship `status: stable` carrying
`ksor.approval: { by: "ksor-starter/<cli version>" }`, and the emitted
`.ksor/governance.yaml` authorises that actor — so `ksor init` followed by
`ksor build` reports **5 admitted to a machine surface** where it reported
**0**._
_WHY: the all-draft starter did not cost one conversational turn, it cost
the entire first build. An adopter's `llms.txt` had an empty
`## Documents`, the `/md/` twins were empty, no document route existed at
all, and a door pointed at that record answered nothing — on the
hello-world, which has to be simple to get started. The claim the empty
build made visible was visible to nobody, because the surfaces that would
have carried it were the surfaces it emptied._
_HOW this stays inside R25 and decision 21: the approver is a PRODUCER,
not a person. `ksor-starter/<version>` is the form `generated.by` already
uses; no human handle appears, so the tool is not recording that somebody
reviewed something. What R25 forbids is a self-asserted string wearing a
schema, indistinguishable from a person who was never there — a producer
id is distinguishable by construction. The trust tier stays `unverified`,
which is the honest word for nobody having checked it, and no `verified`
entry is written. **This is not the starter being pre-approved by the
adopter, and must never be described that way.**_
_WHAT IT COSTS, recorded rather than sold: an actor that is not a person
holds approval authority in the adopter's OWN `.ksor/governance.yaml`
from the moment they scaffold, and stays there until they delete it. And
an adopter who never reads the samples publishes five documents they did
not write, about KSoR rather than about their organisation, on a record
whose whole purpose is settling which copy governs. Neither is fixable by
the tool, so both are disclosed instead — in the emitted README, the
emitted AGENTS.md, a comment in the policy file itself, and the
intake-interview skill, each naming the producer and saying to delete it
once the samples are gone. UNCHANGED is everything the owner writes: a new
document is `draft` and reaches no machine surface until a human approves
it. Reversed by an owner decision recorded here, or by evidence that
adopters are shipping the samples as their own record._
Reversed per clause with evidence, recorded here; the two clauses marked
**owner-only** — the audience leak guarantee, and retiring `sor_id`
against decision 14 — are not reversible without an owner decision.
28. **A retired surface is REMOVED, never deprecated** (owner, 2026-08-26).
Pre-1.0, and the whole population of built records is ours — `migrate` has
never shipped at all (`docs/status.md`) — **false; see the 2026-08-27
revision** — and the owner confirmed there are no external adopters. A deprecation window therefore buys nobody anything
and costs everybody the second code path coding principle 4 forbids. So a
surface this project retires is gone in the release that retires it, and
what replaces it is a REFUSAL naming the fix, never a fallback that keeps
working quietly.
This branch already did it five times and recorded it nowhere, which is why
the rule is being written down rather than invented: the `--knowledge` flag
refuses as an ordinary unknown one rather than warning
(`packages/content/src/commands.ts`); `ksor takedown --export` and
`.ksor-denylist.json` are gone; nine frontmatter keys are refused BY NAME
rather than ignored (`LEGACY_KEYS` in `record/profile.ts`); `audiences:`
and `default_visibility:` refuse with a hint
rather than being read where they used to live (`MOVED_INSTANCE_KEYS`);
`instance.md format: 1` refuses outright.
**What makes this safe rather than merely fast is that removal is paired
with a MIGRATION, not with a warning.** `ksor migrate --write` carries the
record across, and where it cannot know something it refuses by name
instead of guessing (`ksor-migrate-underivable`). A removal with no
migration path is not covered by this decision and stops for a human. The
migration must also carry the adopter's TOOLING, not only their content:
this rule was written the same day an audit found `migrate` fixing the
`build` script and not `refresh`, so a correct upgrade left the adopter's
own gate red — an upgrade that does that is not an upgrade.
**The one exception, and why it is one.** A missing FLOOR refuses; a
missing `min_trust_tier` is NOTICED (`content-gateway/src/gateway-verify.ts`).
The line is guarantee versus capability: without the
floor text a guarantee is broken, and without the parameter every
guarantee still holds and only an affordance is absent. An absence nobody
is told about is one nobody fixes, so it is reported — never silently
tolerated. That is the shape any future affordance takes; it is not a
licence to notice a broken guarantee.
**What it costs, stated plainly:** our own records upgrade or stop. There
is no version of ksor that reads both shapes, by design.
**Reversed the day a record we do not operate is built by a published
release** — from then on a retirement either ships with a migration that
runs unattended, or waits for a major. Not reversible by convenience: "an
adopter might" is not an adopter, and the reversal condition is an event
that can be observed rather than forecast.
_Revision 2026-08-27 (found by the #151/#180 sweep; a correction of the
EVIDENCE, not a reversal — what to do about it is the owner's). The premise
cited the document that contradicts it: `docs/status.md` records
`ksor migrate` as implemented and RELEASED in 0.0.41 and lists it among the
published verbs, and by authority rule 3 that file is the authority on what
is built. **The correction cuts both ways.** It removes half the
justification — what survives there is the owner's confirmation that there
are no external adopters, a real basis but a statement about today rather
than about what the tool now makes possible. It also REPAIRS the safety
clause: "what makes this safe rather than merely fast is that removal is
paired with a MIGRATION" requires `ksor migrate` to exist and ship, so the
entry had been leaning on a shipped migrate while asserting it had never
shipped. That clause is true now rather than aspirational. The decision is
left STANDING and unaltered — its reversal condition is a record we do not
operate being built by a published release, and migrate shipping is not that
event — and this is recorded so the next removal under this rule is made
knowing what actually backs it._
29. **The deploy REGENERATES the lock; it does not verify it** (owner,
2026-08-26). `vercel.json` builds the site with `pnpm build`, which is
`ksor build && <site build>`, so a host regenerates every `index.md` and
`build.lock.json` before the site is built. An adopter can therefore deploy
without ever having run `ksor build`, and the lock committed to their
repository is not necessarily the one that shipped.
Weighed and kept, because the alternative taxes the wrong person. Measured
on a real scaffold: the site build ALONE refuses `ksor-lock-missing` with no
lock and `ksor-lock-stale` when a document changed since one was written
(naming the document), and succeeds on a matching lock writing no tracked
file. So decoupling works — and it would oblige every adopter to run
`ksor build` and commit on EVERY knowledge edit, forever, or watch their
deploy fail. Product principle 7: governance is a ladder, and demanding a
reviewed lock of a level-0 project is a bug, not rigour.
**What is NOT given up.** The record checker runs on the deploy exactly as
it does locally — a record that breaks the profile fails there, exit 1,
nothing written. And the `build_id` that shipped is stamped into the
deployed `llms.txt`, so what was published is always discoverable from the
artifact. What is given up is narrower than it first looks: that the lock
in git is the one that shipped, and therefore that a human reviewed the
`build_id` in a pull request.
**The stricter posture needs no product change**, which is why none was
made: `buildCommand: "pnpm -C system/site build"` is one line in the
adopter's own `vercel.json` (decision 4 — that file is theirs), and the
refusals it relies on already exist and are asserted. It is documented in
`docs/deploying.md` as a choice rather than shipped as a flag.
Two costs recorded rather than argued away. `ksor build` rewrites
`build.lock.json` on EVERY run because `as_of` is the current instant, so a
no-op `pnpm build` leaves git dirty by one line (`--as-of` pins it; the
`build_id` itself is stable for the same tree). And `ksor-lock-stale` can
never fire on a deploy that regenerates the thing it checks — the gate is
real, it is simply not on that path.
**Reversed by the first adopter who needs the deployed `build_id` to have
been reviewed before it shipped** — a regulated record, or an audit that
asks which commit produced a published answer. That is an observable event,
not a forecast, and when it arrives the change is a default flip plus a
migration note, not new machinery.
30. **The shipped DEFAULT stays `gemini-embedding-001` at 1536 dimensions**
(2026-08-27, from the evidence in issue #49). Serves the claim that a
surface offers "a measured floor under which it declines": that floor is a
threshold INSIDE one embedding space, so which space ships decides what
every adopter must re-measure when it changes.
Binds `EMBED_MODEL` and `EMBED_DIM` in `packages/content/src/config.ts` and
nothing else — `embedding.model` and `embedding.dim` stay per-instance keys,
and raising `EMBED_DIM_MAX` is priced here, not forbidden
(`instance.test.ts` holds its two declarations equal). The measurement
behind it lives beside the constant it constrains, in
`packages/content/src/schema.ts`, with its source and retrieval date; this
entry does not restate it.
**Moving is not a version bump.** A different model is a different embedding
space: a re-embed of the whole corpus, and every calibrated `vector_floor`
invalidated — ours and every adopter's — by the same argument the product
invariant makes about copying a constant between corpora. And this record's
gold has no margin to spend on quantisation: the near-miss at 0.683
outscores the weaker in-corpus question at 0.671
(`evals/behavioural.db.test.ts`), the NOT-separable shape `ksor calibrate`
refuses to emit a floor for (`calibrate/math.test.ts`).
**Reversed by either of two things.**
1. **0.1 MTEB points or more**, with both numbers from ONE table or one
benchmark revision — differencing a successor's published score against a
truncation table's row is not a comparison at this resolution. Ten times
the 0.01 the default rests on. Stated in MTEB and never in cosine:
decision 20 uses a hundredth of a cosine to mean something load-bearing.
2. **A reproduced CJK failure.** `-001` takes 2048 input tokens and
`HARD_MAX_CHARS = 4000` can exceed that, surfacing as failed chunks
rather than an error — bounded by `MAX_FAILED_FRACTION = 0.02`, and
visible below that in `chunks.embed_error` and the run's failed count.
Reproduce it before choosing a remedy: `HARD_MAX_CHARS` and `MAX_CHARS`
bound different paths, both are eval-locked globals rather than
per-instance knobs, so re-chunking is not the cheap local fix it looks
like — and `-2`'s 8192-token window is a different embedding space with
the price above.
A successor model shipping is not, by itself, evidence for either. The `-2`
figures cited above come from `research/i18n.md`, whose own frontmatter says
nothing in it is implemented or decided; check them before acting.
31. **The scaffold ships three skills; a skill is measured by a real agent
or it is not kept** (owner-delegated, 2026-09-02; issues #30, #31, #50).
Four independent reviews of the plan to build a system of record, run
before building any of it, converged on what follows.
**Pruned to three.** `make-slides` and `make-summary` are removed: 45% of
all shipped skill text, 21.6% identical to each other by their own
commit's admission, downstream of a record existing, invisible to the
agent surface, unused by every fixture and tutorial, and never shown to
beat their absence — which this file already made the condition for
keeping a skill. Their one rule stays in the emitted AGENTS.md; the site
renders companions unchanged. `format-checker`'s prose is cut to what
AGENTS.md does not say; the program is untouched.
**One skill for a file and a person, not two.** #31 (convert a folder of
PDFs) and #50 (write down what only lives in someone's head) land as
`add-sources` 2.0.0, not as siblings. The record draws no line between
the kinds: an interview attestation in `sources[].resource` passes
`ksor build`; the fidelity rules read identically; and a real owner has
BOTH — the policy PDF and the exception it never mentions — so the person
step runs after every file. A sibling forces the agent to choose before it
knows, and reintroduces the three-way trigger collision #50 warned of.
Conversion verifies with a shipped script (`verify.mjs`: every number,
date and name in the body is in the extraction) rather than a rule the
model is asked to follow; a scanned PDF is refused, never OCR'd and hoped.
**The harness is a real agent, in TypeScript, gated like the database
tier.** `skill-creator`'s `run_eval.py` was proposed and measured: it is a
trigger eval only (its docstring says so), spawns `claude -p` per prompt,
shares one stub directory across parallel workers (1/3 vs 2/2 on the same
query), and the with/without content comparison it describes is an
interactive workflow, not a script. A single model call through the
Gemini seam cannot see the acts that distinguish a good run (which files
it did NOT touch). So: `*.agent.test.ts`, `claude -p` in a fresh
scaffold, two arms, deterministic graders gate and the baseline is
reported. What it cannot measure is stated in the suite: conversational
skills need a scripted owner (a second shape, not built), and the
adopter's own model is whatever they run.
**What it costs.** A one-word reply on the default model measured $0.25,
so the tier pins a mid-tier model and runs on push to main, not per PR.
It authenticates through `claude`'s own login, never an API key (owner,
2026-09-02): a logged-in CLI locally, and in CI a `claude setup-token`
token in `CLAUDE_CODE_OAUTH_TOKEN` — which bare mode does not read, so the
tier never passes `--bare`. That secret is an owner action; until it
exists the tier runs and prints that it skipped. Companion generation no
longer has a skill to fire; an owner asks their agent in plain words and
AGENTS.md carries the rule.
**Reversed** per clause: a companion skill returns with the with/without
comparison this file demands, recorded in its PR; the one-skill cut
reverses if a measured trigger collision shows the person path firing on
prompts meant for the interview; the harness shape reverses if a
conversational harness lands and proves cheaper per finding.
**Open questions — decide independently when the work arrives:** ~~how
retrieval and abstention are implemented for `serve`~~ — decided 2026-08-19,
decision 11: the predecessor kernel converts (revision trail: recorded as
settled "stays Python" 2026-08-17, reversed to "either" 2026-08-18). PyPI
`ksor` is left unclaimed on purpose (a PyPI pending publisher reserves nothing
— only an upload claims a name); revisit only if the exposure changes.
**The two the SECOND record forces** (surfaced 2026-08-20 while renaming the
floor package; neither blocks the content SoR, both must be answered before
`ksor-identity` exists, and neither is a naming question):
1. **Which half of `schema.sql` is governance, not content** (issue #17). The schema owns
two kinds of table. Content-shaped: `content_nodes`, `chunks`, `sources`,
`node_centroids`, `slug_aliases`. Generic to ANY record: `corpora`,
`ingestion_runs`, `schema_meta`, `takedown_denylist`,
`ingest_tenant_grants`, `retrieval_log` — generations, takedown, ingest
authorization and the provenance trail are machinery every SoR needs
IDENTICALLY, and they are where the product's guarantees live. A second
record either forks them (and the guarantees drift per record — the failure
mode) or shares them (and something must own the shared half). The roles are
already namespaced `sor_content_*`, so the split is half-anticipated. Decide
before duplicating, because duplicated governance is the one duplication
this project cannot afford.
2. **The bundling ceiling** (issue #18). Decision 12 inlines the whole kernel into the one
published CLI — ~60MB with ONE record. Three records put content + identity
- praxis and their dependencies into every `npx @panaversity/ksor init`,
including for adopters who will never climb to a served rung at all. The
answer is either selective bundling or separately installable records; what
is NOT available is "keep inlining everything", so the decision arrives with
the second record whether or not it is taken deliberately.
## Product principles
1. **Docs are priority #1.** Agents read the docs before they ever run the
product; for a knowledge system of record, the docs are the product twice
over.
2. **One source, two surfaces.** The site and the MCP surface must render the
same corpus build — never let them read different truths. Adding a surface
must never require editing a corpus.
3. **Identity derives from file path.** A doc's path is its ID, its site route,
and its MCP resource URI. No authored `id:`/`name:` fields — the corpus
check rejects them. _Revision 2026-08-25 (decision 27): stronger for
concepts, with one named exception. `sor_id` is retired, so a concept's id
is its bundle-relative path without `.md` and nothing can override it; `id`
and `name` inside a concept are refused by name (`ksor-legacy-key`). The
exception is the INSTANCE: `instance.md` carries `name`, the machine
identity citations and `llms.txt` use, because the record itself has no
path to derive one from._
4. **Errors are documentation.** Every failure states what is wrong, why the
rule exists, and how to fix it. The CLI's exit codes are a contract
(1 refused, 2 not implemented, 3 environment), and when refusals gain
detail, the first stderr line is a stable machine-readable slug.
5. **Abstention is a feature.** "Not in this corpus" is a correct answer, never
an error, never a licence to fall back on model knowledge.
6. **Provenance is load-bearing — and provenance is not correctness.** Every
build must record the exact corpus that produced it (`build.lock.json`,
lands with `ksor build`); every answer must trace to a governed source.
Provenance proves who-said-when; the expert judgment of whether a source is
right is a separate mechanism — never sell one as the other.
7. **Governance is a ladder, not a gate.** Level 0 works immediately; projects
climb only as far as their domain needs. Demanding level 4 of a level-0
project is a bug, not rigour. _Revision 2026-08-25 (decision 27): the
principle stands and the RUNGS are renamed. There is no numeric 0–4 ladder
any more: there is a conformance floor every record meets (`type`, `title`,
`description`, `status`, `ksor.audience`, and a policy naming approval and
takedown actors — with `Document`, the never-reserved type, as the escape
from owners and sources), and above it the trust rungs each concept climbs
on its own — unverified, machine-confirmed, human-reviewed. "Demanding
level 4 of a level-0 project" becomes: demanding `human-reviewed` of a
record that has not asked anyone to review it._
8. **Discoverability determines whether agents find you at all**: bundled docs,
`llms.txt`, an MCP registry entry, a typed SDK.
## Product invariants
Bought with measurements in the predecessor; they bind each slice of code as
it lands here, and tests assert them from day one of that slice:
- **The generation is the authorization.** Every citation carries it; a
surface refuses content whose generation is not published.
- **Fail closed — once a floor is declared.** A declared-but-uncalibrated
floor refuses. A corpus that declares no floor has the gate off, and the
surface says so honestly (uncalibrated — will not refuse out-of-corpus
questions). Honest absence, never silent weakness.
- **Never copy a calibrated constant between corpora.** Recalibrate; record
the measurement and its date beside the number — and record negative results
beside the constant they explain.
- **Zero chunk overlap.** Concatenating a node's chunks in order reproduces
the body byte-exact.
- **Reproducibility is a testable claim.** Same corpus tree + same toolchain
- same `as_of` ⇒ same `build_id`. Test by building twice and diffing
`build.lock.json`. The `as_of` clause is decision 27's: `build_id` covers
each document's ADMITTED set, so moving `as_of` across an `effective_from`
or a `stale_after` changes what the build publishes and must change the id.
Two runs without `--as-of` differ only in the `as_of` field itself; with
`--as-of` repeated they are byte-identical.
## How we work
1. **Test-driven, red first.** Acceptance and tests are written before the
implementation and watched failing for the right reason; the
implementation's job is to turn exactly those red lights green. Load
$implement-spec before writing the first line. An aspect with no test
planned is a hole in the plan, not a TODO.
2. **Small, composable units.** One responsibility per module; behavior lives
in small pure functions composed upward; the CLI stays a thin caller of
library functions (the boundary suite enforces that nothing imports it).
Prefer composing what exists — net-new code states why composition failed.
3. **Never write the present tense about behaviour that does not run.** If it
is not built, say "will". This is the rule that protects all the others.
4. **One fact, one file** — everywhere else is a pointer.
5. **Cite `file:line` against pinned SHAs, or say you do not know.**
6. **Supersession is visible.** A reversed decision keeps its entry and gains a
revision note; superseded documents live in git history, not the working
tree.
7. **Smallest change that proves the next assumption.**
8. **One obvious way.** Agents sample across options; a golden path is a
compatibility guarantee.
9. **Never carry a mechanism across without asking what it was for** — from
the predecessor or anywhere else.
10. **Governs acts, not artifacts.** Ask of every mechanism: which act does it
constrain, who performs it, and what row exists afterwards proving they
did — never merely what field it adds to a register. Rights checked at
ingest but not at serving, approval attached to a corpus but not to an
answer — those fail this test.
11. **Every change names its business claim** — the promise in "What we claim"
it serves. Work that cannot name which claim it serves does not get built.
**Specs — where they count, never for small things.** A change gets a spec at
`specs/<area>/<feature>/spec.md` only when it alters a public surface (CLI
verbs, scaffold contents, MCP tools, `build.lock.json`, response envelopes),
crosses a package boundary, is expensive to reverse, or will be built
unattended by an agent. A spec is one page: status, the business claim it
serves, the observable contract, acceptance, out-of-scope. Where spec and code
disagree, the code wins and the spec is corrected in the same commit. Specs
cite the research they distill; guard rule 8 enforces the frontmatter
(status + claim). `specs/` appeared with its first spec: `specs/base-env/`.
## Coding principles
1. **Code is liability — and so is context.** Every net-new snippet, file, and
skill earns its right to exist; cut what stops earning it.
2. TypeScript strict, pure ESM, no `require()`, no `any`. **Derive types from
values wherever a value already holds the truth** (`as const`, schema
inference — a hand annotation wider than the value is a downgrade); declare
them **explicitly at the exported boundary** (`isolatedDeclarations`
enforces this) and wherever the type carries a constraint no value
expresses (brands, discriminated unions that encode a protocol). Never
depend on the TypeScript compiler API — TS 7 has no stable one until 7.1
(guard rule 6).
3. Runtime dependencies need a recorded decision (guard rule 5). Wrap
third-party libraries at a boundary module so they stay replaceable.
4. Pre-1.0: prefer breaking changes. Correctness and simplicity over backwards
compatibility; no legacy fallback paths.
5. Comment why, not what. Default to no comment.
6. If a guard fails, fix the violation. Guards have no suppression mechanism;
a rule that must land against existing violations lands together with the
fixes.
7. Package boundaries are enrolled, never implied: every workspace package
appears in `ALLOWED` in `scripts/boundaries.integration.test.ts`, declaring
what it may import.
## Testing
Four tiers by filename convention; pick the tightest tier that can express the
assertion.
- `*.test.ts` — unit, colocated (packages `src/` and `scripts/`): pure, no
fs/subprocess/network (<3s total)
- `*.integration.test.ts` — built artifacts, subprocesses, repo-tree scans,
tmp dirs (~2 min). The `<15s` this line used to claim was measured before the
tier spawned the built CLI per test, packed a tarball and installed it — the
costs that ARE the tier ("the test tier must install the same tree the
artifact installs"). It is a shape, not a budget: a suite belongs here
because of what it touches, never because of what it costs.
- `*.db.test.ts` — real Postgres, gated on `KSOR_DB_URL` (`pnpm test:db`; CI
provides the service). The kernel's guarantees are SQL, so the tier that runs
them against a real database is where they are actually held. A suite here
owns its own scratch DATABASE, named uniquely per run and stamped with the
instant it was made (guard rule 12) — a fixed name let two runs on one
cluster drop each other's database mid-test, and the stamp is what lets the
tier's reaper tell a leak from a live run's database (issue #166). The tier
also carries the suites whose SHAPE is a real socket and a real clock —
`probe-deadline.db.test.ts` opens a listening socket and waits eight real
seconds and reads no `KSOR_DB_URL` at all. They run unconditionally, so a
machine with no database pays their wall clock for nothing; that is the
cheaper half of the trade, because gating them on a variable they never read
would mean the readiness budget is only ever asserted in CI. The unit tier
cannot hold them: it admits neither sockets nor waiting.
- `*.agent.test.ts` — a shipped skill run by a REAL coding agent (`claude -p`)
in a fresh scaffold, with the skill and without it, graded on what it leaves
behind (`pnpm test:agent`; `skill-evals.yml` runs it on push to main and by
hand, never per PR — it spends model tokens). Gated on `CLAUDE_CODE_OAUTH_TOKEN`
in CI or a logged-in `claude` locally, and it announces its own skip. The
three-class split below applies: the deterministic graders GATE (files
touched, the record builds, values verified), cost and the baseline arm are
REPORTED. Decision 31.
The tiers are a contract, not a preference: a file that reads the filesystem
belongs in the second one however small it is. Seven did not, and drifted there
because the unit tier is the fastest to run (round-9 review of PR 43).
The tiers themselves did not change with decision 27; what it added is a fourth
obligation that cuts across them. The record's rules are now executed by THREE
programs — the kernel's `record/` modules, the emitted `check.mjs` built from
them, and the site's byte-copies — so a rule is only held when one conformance
fixture is judged identically by all three, and the drift tests are what make
the copies trustworthy rather than merely present. A rule asserted in one
program alone is the shape decision 18 was written about.
Agent evals — of the served door and of the shipped skills alike — are
CI-only because they spend model tokens, and come in three classes; being
explicit about which class gates is the design:
**behavioural** evals gate (abstains out-of-corpus, citations resolve,
unpublished generations never served); **relevance** evals are reported, never
gating — their gold is generated from the corpus under test, so a wrong rule
would generate a gold question that blesses the wrong rule; **correctness**
evals are externally authored and ratchet — the baseline may only grow.
Out-of-corpus probes must include scope-adjacent near-misses, not only
far-domain questions. Every suite includes at least one question whose only
passing answer is the abstention.
Three rules paid for with shipped defects (post-mortems in
`research/handover-vsor-to-ksor.md`):
- **Assert on shipped bytes and computed values, not behavior alone.**
- **The test tier must install the same tree the artifact installs.**
- **A failing assertion must print the value it actually saw.**
## Documentation
Update docs in the same PR as the behavior change; run `pnpm check:corpus`
before handing off.
Do not rely on training data for claims about ksor. In order: 1 source, types,
and tests · 2 real CLI output · 3 existing docs · 4 merged PRs and the
changelog. `research/` plans are intent, not behavior — cite as "planned".
For third-party systems, fetch current official docs; don't recall them.
Corpus documents name their sources precisely and copy load-bearing values
exactly; superseded documents are marked, never deleted. Any tree, count, or
list rendered into a doc is generated from source with a drift test, or not
rendered at all.
## Changesets and releases
Every PR changing anything under `packages/ksor` needs a changeset — bundled
docs included, they ship in the tarball (and the CI gate watches the whole
package directory). Repo docs and tooling outside `packages/ksor` are exempt.
Patch by default pre-1.0; minor only for public-API breaks. Write the body
for release-notes readers.
Check: `pnpm changeset status --since=origin/main`.
Releases publish only from CI (`release.yml`: changesets action + npm trusted
publishing, full gate runs in the same job). Never run `changeset publish` or
`npm publish` locally; never cancel a running release — the concurrency group
queues.
## Skills
Always-on policy lives in this file; deep workflows live in skills and never
duplicate this file — they go deeper.
- $implement-spec — the implementation discipline: red-first, live
verification, detail pass, truth sweep
- $release — the release airlock, pre-publish testing, and the red-Release
runbook (trusted publisher, org PR-permission, snapshots)
- $find-skills — discover/install ecosystem skills (hash-pinned in skills-lock.json)
- $skill-creator — vendored (anthropics/skills): create, improve, and eval skills
- $mcp-builder — vendored (anthropics/skills): MCP server design and tooling,
for the `serve` slice
The contract for authoring one: frontmatter `name` equals the directory name
(guard rule 3), the `description` is the trigger — name the tasks and phrases,
bump `metadata.version` on every edit, and a new skill must beat its absence
in a with/without comparison recorded in the PR — a skill nobody can show
winning is deleted. Vendored skills (hash-pinned in `skills-lock.json`) keep
their upstream frontmatter untouched.
## Commit and PR style
Imperative, concise commit subjects. PRs describe problem → solution →
behavior for a reviewer, not a file list. Leave PRs in draft; a human marks
ready.
## Authority, and definition of done
1. **The code beats every document.** Where they disagree, correct the
document in the same commit.
2. **This file** is authoritative on vocabulary, decisions, invariants, and
process.
3. **`docs/status.md`** is the only authority on what is actually built.
4. **Superseded documents live in git history, not the working tree.**
Done means: red tests written first are green; acceptance passes on a clean
machine; any document the change made false was corrected in the same commit;
review findings were fixed or recorded, never quietly dropped.
## Do not
- Do not weaken provenance, citation, abstention, or governance to make a test
pass.
- Do not add runtime dependencies without a recorded decision (guard rule 5).
- Do not author `id:`/`name:` fields where the path is the identity — they are
refused by name in a concept (`ksor-legacy-key`). `instance.md`'s `name` is
the one exception, because a record has no path to derive one from.
- Do not edit ALLOWED import graphs without review.
- Do not commit `.only` or skipped tests (guard rule 7 rejects them).
- Do not carry a predecessor mechanism across without asking what it was for,
and never without tests here — conversion is granted (decision 6), blind
copying is not.
- Do not create `knowledge/`, `governance/`, `.ksor/`, `build.lock.json`, or
`instance.md` at this repo's root — those belong to scaffolded projects (the fixture lives under
`workbench/`), and a root `instance.md` additionally makes `ksor init`
refuse `error: nested` anywhere inside the checkout (guard rule 10).
- Do not create GitHub issues/comments or publish packages on your own
initiative.
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
Posts are public.Sign in to post
No one has posted yet. Be the first.

