@panaversity/ksor 0.0.58 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +242 -0
- package/README.md +8 -5
- package/dist/checker/check-main.mjs +142 -136
- package/dist/cli.mjs +842 -211
- package/dist/{gateway-api-DvM2mVtD-BXmUS4T8.mjs → gateway-api-CEsb-e8z-Ch6oMaq2.mjs} +74 -7
- package/dist/gateway.d.mts +2 -2
- package/dist/gateway.mjs +1 -1
- package/docs/building.md +157 -0
- package/docs/deploying.md +14 -2
- package/docs/index.md +5 -1
- package/docs/ingesting.md +29 -2
- package/docs/upgrading.md +15 -1
- package/package.json +3 -3
- package/templates/scaffold/.agents/skills/add-sources/verify.mjs +6 -1
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +140 -134
- package/templates/scaffold/.claude/skills/add-sources/verify.mjs +6 -1
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +140 -134
- package/templates/scaffold/AGENTS.md +21 -5
- package/templates/scaffold/README.md +5 -1
- package/templates/scaffold/env.example +3 -0
- package/templates/scaffold/system/site/next-no-telemetry.mjs +27 -0
- package/templates/scaffold/system/site/package.json +2 -2
- package/templates/scaffold/system/site/record/change-control.ts +319 -0
- package/templates/scaffold/system/site/record/index.ts +8 -0
- package/templates/scaffold/system/site/record/lock.ts +37 -0
- package/templates/scaffold/system/site/record/refusal.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,247 @@
|
|
|
1
1
|
# @panaversity/ksor
|
|
2
2
|
|
|
3
|
+
## 0.0.60
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2a7ef92: Five things a live walk of the published 0.0.59 found, fixed.
|
|
8
|
+
|
|
9
|
+
**`verify.mjs` reported ordinary markdown as an invented name.** Its name regex
|
|
10
|
+
let `\s+` cross a blank line, so a `## Meals` heading followed by a paragraph
|
|
11
|
+
opening `On travel…` was extracted as the name "Meals On" and reported as
|
|
12
|
+
changed-or-introduced. That fires on the first document an agent converts —
|
|
13
|
+
the check meant to make conversion trustworthy was crying wolf. Names are now
|
|
14
|
+
capitalised words on ONE line; a name the source never mentions is still caught.
|
|
15
|
+
|
|
16
|
+
**A scaffold followed verbatim published its first generation untraceably.**
|
|
17
|
+
`ksor init` runs `git init` and leaves zero commits, and its own epilogue went
|
|
18
|
+
install → dev → provision → refresh with no commit in between — so every
|
|
19
|
+
adopter's first publish said `source: unspecified` and skipped the R23
|
|
20
|
+
change-control check, which had no history to compare against. The epilogue now
|
|
21
|
+
says to commit before publishing, which is the whole fix.
|
|
22
|
+
|
|
23
|
+
**A plain build left a stale bundle tree unmentioned.** `ksor build` recomputes
|
|
24
|
+
every `bundles[].sha256` in the lock but only `--bundles` writes the directory,
|
|
25
|
+
so after one `--bundles` run and any ordinary build the lock claimed a digest
|
|
26
|
+
nothing on disk produced, while the tree that exists to be SENT somewhere aged
|
|
27
|
+
silently. A plain build now says which build the directory came from and how to
|
|
28
|
+
refresh it. Reported, never deleted: it is the adopter's output and may be
|
|
29
|
+
mid-handover.
|
|
30
|
+
|
|
31
|
+
**Tutorial 01's first build block was one line short.** R23 landed the day after
|
|
32
|
+
that walk, so the shipped block omitted `change-control: not checked` on a page
|
|
33
|
+
whose headline claim is that every output was pasted as it appeared. Re-captured
|
|
34
|
+
on 0.0.59, with a paragraph on what both honesty lines mean and when they go.
|
|
35
|
+
|
|
36
|
+
**The dev server's `/llms.txt` does not change after an approval.** Not a bug and
|
|
37
|
+
not fixable in the route: `output: "export"` requires a static route handler, so
|
|
38
|
+
`pnpm dev` computes it once per process while the document's page beside it
|
|
39
|
+
updates. Stated in the emitted README's troubleshooting table and in the tutorial
|
|
40
|
+
step whose own prompt is "Why isn't my refund policy in llms.txt?" — verified by
|
|
41
|
+
testing both alternatives, each of which breaks the export.
|
|
42
|
+
|
|
43
|
+
- 6c47618: Measure WHICH skill a real agent reaches for, and record what the first sweeps
|
|
44
|
+
found.
|
|
45
|
+
|
|
46
|
+
A live walk of the published 0.0.59 reported that `add-sources` did not fire on
|
|
47
|
+
its headline prompt. The tempting repair is to reword the description until it
|
|
48
|
+
does, which is a guess. This is the instrument that replaces the guess: N runs
|
|
49
|
+
per phrase, in a fresh scaffold with all three skills present, graded on which
|
|
50
|
+
skill the agent actually invoked, across more than one model. Reported, never
|
|
51
|
+
gating — a model is stochastic and a threshold over a handful of runs flakes.
|
|
52
|
+
|
|
53
|
+
**The model is a column, because the answer depends on it.** Same phrase, same
|
|
54
|
+
scaffold, same harness: `claude-sonnet-5` fired `add-sources` 3/3 where
|
|
55
|
+
`claude-opus-5` fired nothing 0/2. The walk used the CLI default and the first
|
|
56
|
+
probe pinned Sonnet, which is why they disagreed — neither was wrong, and
|
|
57
|
+
neither alone measured the trigger.
|
|
58
|
+
|
|
59
|
+
**The finding is narrower than the walk suggested.** `add-sources` fires on four
|
|
60
|
+
of five phrases on both models, and both controls behave: a different skill wins
|
|
61
|
+
the intake phrase, and nothing fires on a question about the repo itself. It
|
|
62
|
+
misses exactly one shape on Opus — the owner pointing at a file already in the
|
|
63
|
+
repo and naming a destination.
|
|
64
|
+
|
|
65
|
+
**And the obvious repair does not work.** Naming that shape in the description
|
|
66
|
+
was tried and measured: unchanged at 0/3. So the cause is not the wording — an
|
|
67
|
+
instruction concrete enough to act on gets acted on, and no skill is consulted.
|
|
68
|
+
The clause was reverted rather than kept, because it is resident context in
|
|
69
|
+
every session and bought nothing a measurement can see. The negative result is
|
|
70
|
+
recorded beside the rows it explains.
|
|
71
|
+
|
|
72
|
+
Also fixed while building it: the CLI can emit raw control characters inside its
|
|
73
|
+
JSON, which `JSON.parse` rejects outright — the harness lost the whole transcript
|
|
74
|
+
to a stray byte. It now falls back to a scrubbed parse.
|
|
75
|
+
|
|
76
|
+
Test infrastructure only; nothing an adopter installs behaves differently.
|
|
77
|
+
|
|
78
|
+
## 0.0.59
|
|
79
|
+
|
|
80
|
+
### Patch Changes
|
|
81
|
+
|
|
82
|
+
- 4d7dad7: The agent tier authenticates through `claude`'s own login, never an API key.
|
|
83
|
+
|
|
84
|
+
Where the repository needs model inference — today, the skill evals — it uses
|
|
85
|
+
`claude -p`: a developer's logged-in CLI locally, and in CI a long-lived token
|
|
86
|
+
from `claude setup-token` in `CLAUDE_CODE_OAUTH_TOKEN`. The tier no longer reads
|
|
87
|
+
`ANTHROPIC_API_KEY` and no longer passes `--bare`, because bare mode does not
|
|
88
|
+
read the OAuth token (per the official headless docs) and would have quietly
|
|
89
|
+
fallen back to needing the key it is not supposed to have. A guard holds both.
|
|
90
|
+
|
|
91
|
+
The pending owner action changes with it: a `CLAUDE_CODE_OAUTH_TOKEN` repository
|
|
92
|
+
secret, not an API key. Test infrastructure only; nothing an adopter installs
|
|
93
|
+
behaves differently.
|
|
94
|
+
|
|
95
|
+
- 5bf3733: `ksor build --bundles` is implemented (issue #158). It used to parse the flag
|
|
96
|
+
and exit `2`; it now runs an ordinary build and additionally writes one OKF
|
|
97
|
+
bundle per viewer under `.ksor/out/bundles/<viewer>/` — `public`, and
|
|
98
|
+
`[public, X]` for each audience X your `.ksor/governance.yaml` registers. A
|
|
99
|
+
bundle holds exactly what that viewer's machine surfaces publish: the admitted
|
|
100
|
+
concepts (stable, effective, unexpired, not taken down, audience overlapping),
|
|
101
|
+
their companions, the assets their bodies reference, and every `index.md`
|
|
102
|
+
regenerated for that filtered tree with `okf_version` at the root. No byte of a
|
|
103
|
+
concept excluded for AUDIENCE reaches it — not a title, a path, a description
|
|
104
|
+
or an asset — because the record checker refuses a link that widens audience
|
|
105
|
+
before a bundle is planned. A document excluded for a lifecycle or ledger
|
|
106
|
+
reason is different, and deliberately: bodies are copied verbatim, never
|
|
107
|
+
rewritten, so a link to a draft or a taken-down document keeps that path and
|
|
108
|
+
the build reports the dangling link instead of editing your prose. Any OKF
|
|
109
|
+
consumer reads a bundle with no ksor in the loop. The directory is replaced on
|
|
110
|
+
every run, a copy of `build.lock.json` is written beside the bundles, and the
|
|
111
|
+
scaffold's `.ksor/*` rule already gitignores it.
|
|
112
|
+
|
|
113
|
+
`build.lock.json` gains `bundles[]` — one `{ viewer, sha256, files }` per
|
|
114
|
+
viewer, recorded on EVERY build whether or not the flag was passed, so a
|
|
115
|
+
bundle directory can be matched to the publication that produced it. The
|
|
116
|
+
digest is sha256 over the JSON of the bundle's sorted `[path, sha256]` pairs.
|
|
117
|
+
`build_id` is unchanged: the bundles are a function of what it already
|
|
118
|
+
hashes.
|
|
119
|
+
|
|
120
|
+
**Upgrading:** a lock written by an earlier ksor lacks the key, so `ksor build`
|
|
121
|
+
refuses it as `ksor-lock-invalid` and says to delete it. It does NOT regenerate
|
|
122
|
+
one it cannot read — the lock is also a takedown baseline, and a baseline
|
|
123
|
+
nothing can parse is one that quietly holds nothing. So the first `pnpm build`
|
|
124
|
+
after upgrading (`ksor build && <site build>`, which is the deploy command too)
|
|
125
|
+
is red until the stale `build.lock.json` is gone. `ksor migrate` now offers that
|
|
126
|
+
deletion like any other change it carries, so the four steps in
|
|
127
|
+
`docs/upgrading.md` cover it; deleting the file by hand and rebuilding does the
|
|
128
|
+
same thing.
|
|
129
|
+
|
|
130
|
+
Two new refusals, both raised on EVERY build — not only under `--bundles` —
|
|
131
|
+
because the lock lists `bundles[]` either way and a digest for a directory the
|
|
132
|
+
tool refuses to write would be a provenance claim about something that cannot
|
|
133
|
+
exist. `ksor-audience-identifier-invalid`: a registered audience that cannot
|
|
134
|
+
name a bundle directory (`../x`, `.hidden`, `-x`, `internal.`, or
|
|
135
|
+
`build.lock.json`, whose name the lock copy beside the bundles already holds) —
|
|
136
|
+
an identifier must start with a letter or a digit, may then use letters, digits,
|
|
137
|
+
`-`, `_` and `.`, and may not END in `.`, which Windows strips from a path
|
|
138
|
+
segment, so `internal.` and `internal` would be one directory there.
|
|
139
|
+
`ksor-audience-identifier-collides`: two registered audiences differing only in
|
|
140
|
+
case, such as `internal` and `Internal`, which are two viewers in your policy
|
|
141
|
+
and ONE directory on macOS and Windows — the second bundle would merge into the
|
|
142
|
+
first, leaving a directory that holds concepts its viewer may not read.
|
|
143
|
+
|
|
144
|
+
- d896f67: The skill harness gains two fixtures a baseline plausibly gets wrong, and is
|
|
145
|
+
parametrised over a CASES table. Nothing an adopter installs changes: the
|
|
146
|
+
fixtures, the graders and the baseline live under `src/evals/`, outside the
|
|
147
|
+
tarball.
|
|
148
|
+
|
|
149
|
+
Three armed runs on the clean two-page PDF had shown both arms — the agent with
|
|
150
|
+
`add-sources` and the agent without it — passing every deterministic gate; a
|
|
151
|
+
harness that cannot tell the arms apart is measuring the fixture, not the
|
|
152
|
+
skill. So `expense-policy-hard.pdf` is built out of the acts a careless
|
|
153
|
+
conversion fails a deterministic gate on: a five-row per-diem table (a dropped
|
|
154
|
+
row is a value `verify.mjs` cannot see missing), the payment window stated
|
|
155
|
+
twice and differently — "10 working days" in §2, "ten (10) business days" in §5
|
|
156
|
+
— which the skill says stays two statements, flagged; `1,250` beside `1.250` on
|
|
157
|
+
one row, where a misread makes them equal and a substring check cannot tell; a
|
|
158
|
+
threshold with a thousands separator; and a running footer on both pages.
|
|
159
|
+
`scanned-policy.pdf` is the same policy rasterised — two pages of picture, no
|
|
160
|
+
text layer — for which the skill's instruction is to stop and tell the owner.
|
|
161
|
+
|
|
162
|
+
`skill-cases.ts` is the table: fixture, prompt, the outcome class a correct run
|
|
163
|
+
belongs to (`converted` or `refused`), and the gates that apply. Its body
|
|
164
|
+
gates are pure functions, so a unit suite mutation-tests each one — smoothing
|
|
165
|
+
the two statements, misreading the figure, dropping a row, dropping or
|
|
166
|
+
misreading a thousands separator, keeping the footer, inventing a currency —
|
|
167
|
+
and watches exactly the gate built for it go red. The agent suite also asserts
|
|
168
|
+
the fixtures are what they claim: each committed `.txt` is its PDF's
|
|
169
|
+
`pdftotext -layout` extraction, and the scanned PDF yields only whitespace. For
|
|
170
|
+
the scanned case the WITH arm's correct outcome — wrote nothing, named the
|
|
171
|
+
missing text layer — is the pass; what the baseline did is reported.
|
|
172
|
+
|
|
173
|
+
Not run here: the armed arms. `SKILL_BASELINE` carries no row for either new
|
|
174
|
+
fixture; the hard fixture's first armed run is pending and its row lands with
|
|
175
|
+
the run that produces it.
|
|
176
|
+
|
|
177
|
+
RESULTS
|
|
178
|
+
|
|
179
|
+
- 3ba4acc: `ksor-generated-stale`: the first change-control verification (KSP R23). A `stable` document whose body differs from a committed version that was `stable` is now refused by `ksor build` and `ksor ingest` unless its `generated.at` is strictly LATER than that version's (the scaffold's `pnpm check` stays the format gate and does not run it), naming the commit whose body differs, its instant and the stamp. Leaving the stamp alone and moving it BACKWARD are refused alike: neither advances it, and a backdated stamp leaves the old approval post-dating the new text — the exact reading R23 exists to prevent. The fix it prints is the whole of it: set `generated.at` to an instant after the edit, then re-approve, because `ksor.approval.at` may not precede it (`ksor-generated-after-approval`, unchanged). Only the body is compared, so adding a `verified` entry or re-approving is not an edit; a document stable for the first time, or renamed, has no history to compare and passes; every committed version is read, so an edit committed without a bump is refused on a clean tree too.
|
|
180
|
+
|
|
181
|
+
Where history cannot be read — no repository, no commit yet, a container without `.git`, a shallow clone — each verb prints `change-control: not checked` (or how many versions a shallow clone let it read) beside its verdict instead of passing. Who approved is still checked against the policy alone: every envelope keeps `approval.checked: "policy"` until R22/R25 have an identity source to verify against.
|
|
182
|
+
|
|
183
|
+
- db39c11: Switch Next.js telemetry off in the scaffold, and make the README describe the
|
|
184
|
+
scaffold that ships.
|
|
185
|
+
|
|
186
|
+
**Nothing phones home — now true of the artifact, not only of the pitch.**
|
|
187
|
+
Next.js ships with anonymous usage telemetry ON, so every site `ksor init`
|
|
188
|
+
emitted posted to `telemetry.nextjs.org` on its first `build` and `dev` while
|
|
189
|
+
the README promised nothing phones home. The site's two scripts now run next
|
|
190
|
+
through `system/site/next-no-telemetry.mjs`, a dependency-free wrapper that
|
|
191
|
+
hands it an environment carrying `NEXT_TELEMETRY_DISABLED=1`; that is where
|
|
192
|
+
every package manager's root script and the deploy's `pnpm build` end up, and
|
|
193
|
+
`.env.example` says so. It is the process environment rather than
|
|
194
|
+
`next.config.mjs` on purpose: read in 16.3.3's source, `next build` constructs
|
|
195
|
+
its telemetry after loading the config, but the `next dev` parent process never
|
|
196
|
+
loads the config and still records a session event on exit — a config-time
|
|
197
|
+
assignment would have left that one in place with nothing red. And it is a file
|
|
198
|
+
rather than a `NEXT_TELEMETRY_DISABLED=1 next …` prefix in `package.json`
|
|
199
|
+
because that prefix is POSIX shell syntax: cmd.exe refuses it under pnpm and
|
|
200
|
+
npm on Windows, a platform CI walks `ksor init` on, so the prefix would have
|
|
201
|
+
traded a quiet leak for a loud break. An existing scaffold gets the switch from
|
|
202
|
+
`ksor migrate --write-site`, which offers the wrapper and the two scripts as a
|
|
203
|
+
diff.
|
|
204
|
+
|
|
205
|
+
**The README no longer contradicts the tree.** Five sentences described a
|
|
206
|
+
product that does not ship, each now corrected and held by a test against the
|
|
207
|
+
thing that decides it: `pnpm serve` is `ksor serve` alone (provisioning is
|
|
208
|
+
`pnpm provision`, publishing is `pnpm refresh`, and ingest reports `unchanged`
|
|
209
|
+
only when documents, governance, toolchain AND source commit all match the
|
|
210
|
+
serving generation); the gate paragraph names the browser, Postgres, Windows,
|
|
211
|
+
npm/bun and container acceptance CI has run for weeks instead of promising
|
|
212
|
+
them, two-way against the workflow's job names; an authored `log.md` is refused
|
|
213
|
+
(`ksor-reserved-name`) and never generated, while `index.md` is generated by
|
|
214
|
+
`ksor build` and drift-checked; the hello-world tutorial row says Part 2 needs a
|
|
215
|
+
free Postgres and a free embedding key (the package README's copy too); and the
|
|
216
|
+
project-structure tree is labelled abridged, with every path it names asserted
|
|
217
|
+
to exist in a scaffold freshly emitted by the built CLI.
|
|
218
|
+
|
|
219
|
+
- 1c336d2: Honest absence on the served rung: the door, the write plane and `ksor takedown` now say what is missing instead of coming up green about nothing.
|
|
220
|
+
|
|
221
|
+
- **`ksor serve` names what it is serving.** The boot block gains a `generation` line — `generation NONE — nothing published; run pnpm refresh` on a record that was provisioned and never ingested (the state `ksor init`'s own next steps leave an adopter who skipped the publish step), or `generation 1 · 7 nodes · source <sha>` once something is. The remedy is spelled for the manager that ran `serve` (`npm run refresh`, `bun run refresh`, or `ksor ingest --flip` with no manager in the loop). `/health` carries the same `generation` field, re-read on every readiness probe, so a `refresh` after boot shows without a restart. The block's value column moved two characters right to fit the longer label.
|
|
222
|
+
- **A missing provider key opens with its slug.** `ksor serve`, `ksor ingest` and `ksor calibrate` exit 3 as before, but the first stderr line is now `error: ksor-provider-key-missing` — the stable name every other refusal opens with — followed by the same sentence naming the variable.
|
|
223
|
+
- **`search` on an empty record answers `reason: "unpublished"` before it embeds.** The question "is anything published?" is now asked first, so a never-ingested record reports its own emptiness rather than the provider's outage, and the provider is not paid for a question no row could match.
|
|
224
|
+
- **`ksor takedown --ledger` never resolves a DSN**: it reads the committed `.ksor/takedowns.yaml` on every rung, including the record `ksor init` emits (which names `KSOR_DB_URL` from birth). `--list` reads the door's denylist rows when the DSN is set and otherwise the ledger's denials, each labelled `not applied (no database)`. A denial itself still needs a named actor and, on a record whose DSN is unset, either the DSN or `--file-only`.
|
|
225
|
+
- **`ksor calibrate --help` prints the whole verb** — both invocation forms, the `--check` paragraph, and the description — instead of the first form's flags alone, and a heading now matches the verb WHOLE — so `ksor g --help`, a verb that does not exist, prints the whole usage rather than `grant`'s block under exit 0. Docs corrected: the scaffold README names `--ooc-file` for scope-adjacent out-of-corpus questions, and `ingesting.md` says the zero-LLM calibration door is not zero-key.
|
|
226
|
+
|
|
227
|
+
- babf416: Five tests now hold what they claimed. Nothing served changes; what changes is what a green run proves.
|
|
228
|
+
|
|
229
|
+
- **The audit-degraded signal is held on every serving arm, and on the wire.** `search`'s hit arm, its abstained arm, `read` and `outline` each answer `audit: "degraded"` when their §7 audit row is shed, and drop the field once it lands — asserted through real Postgres, with the landed state proven by the rows themselves. The three tool output schemas are driven with the real handlers' replies in both states, so a field the service emits and the schema refuses can no longer pass unseen.
|
|
230
|
+
- **Guard rule 12 evaluates each suite's own scratch-database expression** into the name it will mint and hands THAT to the reaper's parser, instead of a literal the guard wrote for itself. `randomBytes(2)` — both halves present, four hex characters where the grammar wants six — now refuses naming the evaluated name; `["ksor", …].join("_")` builders are read too.
|
|
231
|
+
- **`sync-status-version` refuses a prerelease by name** (`ksor-status-version-prerelease`): a snapshot such as `0.0.1-dev-…` never reaches `docs/status.md`, which names only what a plain `npm install` resolves. Its core is a pure function with a colocated unit test.
|
|
232
|
+
- **`probe-deadline` runs on a fake clock in the unit tier** — the file went from 8.2s to 0.3s — and asserts the deadline fires AT the budget rather than within two seconds of it. One real-clock case, a pool against a socket that accepts and never speaks, lives in the db tier.
|
|
233
|
+
- **Every `KSOR_E2E`-gated browser suite says how to run it** — the playwright install from `packages/ksor`, then the `KSOR_E2E=1` command — and a root `pnpm test:e2e` runs all three. The set is now read from the tree rather than kept by hand in three places: a suite that gates on `KSOR_E2E` and is missing from `pnpm test:e2e` or from CI fails by name, as does a skip note that prints a command for a different file.
|
|
234
|
+
|
|
235
|
+
- 37b57c3: Tutorial 3, _Governance in practice_ (`docs/tutorials/03-governance-in-practice.md`), walked end to end on a scaffold with no database and no key and pasted as it ran: a second audience (`internal`) and what the public and employee site builds each put in `llms.txt`; a `verified` entry and the trust tier it moves; `ksor.effective_from` and `stale_after` under `ksor build --as-of`, with the lock diffed at one instant twice and at two instants; a deprecated document and its successor as the page shows them; a takedown written to the ledger with `--file-only`, the `ksor-takedown-dangling` refusal on a renamed file, and a revocation. Fourteen refusals fire — eleven on the path it walks, three on branches it names — each identified by its slug and most pasted with the why and fix the CLI prints. Both README tutorial tables gain a row for it, so the npm package page lists all four.
|
|
236
|
+
|
|
237
|
+
Two tests grow with it: `skill-triggers.integration.test.ts` accounts for every prompt the tutorial gives (one fires `add-sources`; the rest are governance acts no skill mediates), and `docs-truth.integration.test.ts` holds the new file to the rule that a printed `build_id` says what moves it.
|
|
238
|
+
|
|
239
|
+
- 0c51d0b: Tutorial 4, _Serve it — with a floor_, joins the series (`docs/tutorials/04-serve-it.md`): the served rung walked end to end on the three-document record tutorial 2 leaves behind — the `generation NONE` boot block on a provisioned-but-unpublished door, `refresh`, two `ksor calibrate` measurements (the far-domain one that prints a floor and the scope-adjacent one that refuses to and finds an unwritten rule), the pasted floor, three questions from a real coding agent with the near-miss refused, `calibrate --check`, and a takedown between two questions. Every output pasted as it appeared.
|
|
240
|
+
|
|
241
|
+
The package's own tests grow with it: the skill-trigger accounting now covers tutorial 4's prompts (a new prompt fails until someone says which skill answers it), and the `build_id` proximity rule holds the new document too.
|
|
242
|
+
|
|
243
|
+
**Fixed while writing it: `ksor calibrate` prints the built-in-probe caveat again.** A run with no `--ooc-file` scores against the twenty shipped far-domain probes, and the caveat that says so — the one line standing between an operator and a floor blessed by questions nobody would ever bring to their record — was suppressed, because the run was classified as having been given probes. Anyone who calibrated without `--ooc-file` was told the margin was measured against supplied probes when it was not; the recommended floor is unchanged, the warning above it is not. Both READMEs list tutorial 4, and hello world's `ksor serve` block is re-pasted in the shape the door prints today.
|
|
244
|
+
|
|
3
245
|
## 0.0.58
|
|
4
246
|
|
|
5
247
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -49,15 +49,17 @@ where it came from:
|
|
|
49
49
|
walks all of that in about fifteen minutes. Every command and output in it was
|
|
50
50
|
run and pasted as it appeared. The envelopes above show the SHAPE of an answer
|
|
51
51
|
and a refusal; the refusal needs a calibrated floor, which hello world defers
|
|
52
|
-
to
|
|
52
|
+
to **[Serve it — with a floor](https://github.com/panaversity/ksor/blob/main/docs/tutorials/04-serve-it.md)**.
|
|
53
53
|
|
|
54
|
-
The
|
|
54
|
+
The five so far, in reading order — pick by what you want from it:
|
|
55
55
|
|
|
56
56
|
| | read this if |
|
|
57
57
|
| --- | --- |
|
|
58
58
|
| [00 · Introduction](https://github.com/panaversity/ksor/blob/main/docs/tutorials/00-introduction-to-ksor.md) | you want to understand why this exists — no technical background needed |
|
|
59
|
-
| [01 · Hello world](https://github.com/panaversity/ksor/blob/main/docs/tutorials/01-hello-world.md) | you want to see it work in fifteen minutes —
|
|
59
|
+
| [01 · Hello world](https://github.com/panaversity/ksor/blob/main/docs/tutorials/01-hello-world.md) | you want to see it work in fifteen minutes — Part 1 needs only Node; Part 2 adds a free Postgres and a free embedding key |
|
|
60
60
|
| [02 · Make it yours](https://github.com/panaversity/ksor/blob/main/docs/tutorials/02-make-it-yours.md) | you finished hello world and want a record that is only yours — a file in, a person's knowledge in, the samples out |
|
|
61
|
+
| [03 · Governance in practice](https://github.com/panaversity/ksor/blob/main/docs/tutorials/03-governance-in-practice.md) | you have a record of your own and want to govern it — a second audience, a review, an effective date, a takedown and the refusals that fire |
|
|
62
|
+
| [04 · Serve it — with a floor](https://github.com/panaversity/ksor/blob/main/docs/tutorials/04-serve-it.md) | you want the headline claim by hand — measure the line under which your record declines, then watch an agent be refused |
|
|
61
63
|
|
|
62
64
|
One command emits a complete governed project: the record (`knowledge/`,
|
|
63
65
|
plain CommonMark), a working documentation site with hot reload, offline
|
|
@@ -71,8 +73,9 @@ scaffold is deterministic and offline, and every refusal explains itself.
|
|
|
71
73
|
> runs the MCP server over a built record (with `ingest`/`schema`/`calibrate`/
|
|
72
74
|
> `gc` — the climbed rung, needing Postgres and a provider key). Only `dev` is
|
|
73
75
|
> designed, not implemented — it prints an honest notice and exits `2` (inside
|
|
74
|
-
> a scaffolded project, `pnpm dev` covers local work)
|
|
75
|
-
> `
|
|
76
|
+
> a scaffolded project, `pnpm dev` covers local work). `build --bundles`
|
|
77
|
+
> writes one OKF bundle per canonical viewer — `public`, and `[public, X]` for
|
|
78
|
+
> each registered audience X — for exchange.
|
|
76
79
|
> [`docs/status.md`](https://github.com/panaversity/ksor/blob/main/docs/status.md)
|
|
77
80
|
> and the released version number are authoritative for the exact released
|
|
78
81
|
> functionality.
|
|
@@ -10892,7 +10892,7 @@ var require_public_api = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
10892
10892
|
exports.stringify = stringify;
|
|
10893
10893
|
}));
|
|
10894
10894
|
//#endregion
|
|
10895
|
-
//#region ../content/dist/check-
|
|
10895
|
+
//#region ../content/dist/check-Nd8zkMvH.mjs
|
|
10896
10896
|
var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
10897
10897
|
var composer = require_composer();
|
|
10898
10898
|
var Document = require_Document();
|
|
@@ -12851,6 +12851,139 @@ function nearMissOf(baseName) {
|
|
|
12851
12851
|
return null;
|
|
12852
12852
|
}
|
|
12853
12853
|
/**
|
|
12854
|
+
* The takedown ledger's OTHER baseline: every entry any COMMITTED version of
|
|
12855
|
+
* `.ksor/takedowns.yaml` has ever carried.
|
|
12856
|
+
*
|
|
12857
|
+
* The committed lock is a baseline too, and a good one — it holds each entry's
|
|
12858
|
+
* digest, so an entry retargeted in place is caught. What it cannot do is prove
|
|
12859
|
+
* that an entry was never deleted, because the lock travels in the SAME change
|
|
12860
|
+
* as the ledger: delete the row, recompute `ledger_sha256`, empty
|
|
12861
|
+
* `ledger_entries`, and the two agree with each other about a denial that is
|
|
12862
|
+
* gone. Only history remembers.
|
|
12863
|
+
*
|
|
12864
|
+
* The baseline may be INCOMPLETE only if it SAYS so: every version history
|
|
12865
|
+
* holds is read, or `entries` comes back null and the caller reports that it
|
|
12866
|
+
* could not verify. A version silently skipped would contribute neither
|
|
12867
|
+
* digests nor ids while the answer still read "verified"
|
|
12868
|
+
* (`git-ledger.integration.test.ts`).
|
|
12869
|
+
*
|
|
12870
|
+
* This lives in the record module because THREE surfaces need the same answer —
|
|
12871
|
+
* `ksor build`, the emitted checker, and the site's stage (decision 19: a
|
|
12872
|
+
* surface that refuses must refuse on both surfaces). Plain `git log` / `git
|
|
12873
|
+
* show`, so nothing here needs installing.
|
|
12874
|
+
*/
|
|
12875
|
+
const LEDGER = ".ksor/takedowns.yaml";
|
|
12876
|
+
/**
|
|
12877
|
+
* `spawnSync` defaults to a 1 MB stdout buffer, and past it the child is KILLED
|
|
12878
|
+
* — `status` comes back null, so the query reads as a failure. A ledger with a
|
|
12879
|
+
* few thousand entries, or one entry carrying a long reason, clears 1 MB
|
|
12880
|
+
* easily, and the version was then dropped from the baseline while the caller
|
|
12881
|
+
* was still told history had been verified. The ceiling stays finite on
|
|
12882
|
+
* purpose: past it this returns null, which is a state the caller SAYS.
|
|
12883
|
+
*/
|
|
12884
|
+
const MAX_BUFFER$1 = 67108864;
|
|
12885
|
+
/** One git query, read-only. Null on any non-zero exit, including no git at all. */
|
|
12886
|
+
function git(root, args) {
|
|
12887
|
+
const r = spawnSync("git", [...args], {
|
|
12888
|
+
cwd: root,
|
|
12889
|
+
encoding: "utf8",
|
|
12890
|
+
maxBuffer: MAX_BUFFER$1
|
|
12891
|
+
});
|
|
12892
|
+
return r.status === 0 ? r.stdout : null;
|
|
12893
|
+
}
|
|
12894
|
+
function historicLedger(root) {
|
|
12895
|
+
const inside = git(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
12896
|
+
if (inside === null || inside.trim() !== "true") return {
|
|
12897
|
+
repository: false,
|
|
12898
|
+
shallow: false,
|
|
12899
|
+
entries: null,
|
|
12900
|
+
unreadable: null
|
|
12901
|
+
};
|
|
12902
|
+
const shallow = (git(root, ["rev-parse", "--is-shallow-repository"]) ?? "").trim() === "true";
|
|
12903
|
+
const born = git(root, [
|
|
12904
|
+
"rev-parse",
|
|
12905
|
+
"--verify",
|
|
12906
|
+
"--quiet",
|
|
12907
|
+
"HEAD"
|
|
12908
|
+
]) !== null;
|
|
12909
|
+
const entries = shallow ? null : born ? historicEntries(root) : [];
|
|
12910
|
+
return {
|
|
12911
|
+
repository: true,
|
|
12912
|
+
shallow,
|
|
12913
|
+
entries,
|
|
12914
|
+
unreadable: entries !== null ? null : shallow ? "shallow" : "unreadable"
|
|
12915
|
+
};
|
|
12916
|
+
}
|
|
12917
|
+
/**
|
|
12918
|
+
* Every id history has ever recorded, each with the text it carried the FIRST
|
|
12919
|
+
* time it was written. A version that parses contributes each entry's digest,
|
|
12920
|
+
* so an entry EDITED in place is caught, not only one deleted; a version that
|
|
12921
|
+
* no longer parses still contributes its ids, read permissively — the point
|
|
12922
|
+
* there is that an id once written never disappears.
|
|
12923
|
+
*
|
|
12924
|
+
* FIRST, not every: keying this by `id\tdigest` kept one baseline entry per
|
|
12925
|
+
* version an id ever had, so a tamper that was COMMITTED and then UNDONE left
|
|
12926
|
+
* two digests for one id, the restored entry matched only one of them, and
|
|
12927
|
+
* `ksor-ledger-amended` fired for good. The record became permanently
|
|
12928
|
+
* unbuildable — by a tamper that had already been put right — and the only
|
|
12929
|
+
* escape was rewriting git history, which is not a remedy a refusal may
|
|
12930
|
+
* demand (found in review, 2026-08-25).
|
|
12931
|
+
*
|
|
12932
|
+
* Taking the OLDEST is what makes the guarantee both enforceable and
|
|
12933
|
+
* escapable. It still refuses a committed tamper (the baseline is what the
|
|
12934
|
+
* entry said when it was written, so committing the edit does not launder
|
|
12935
|
+
* it), and the remedy it names — put the entry back — now actually clears
|
|
12936
|
+
* it. Taking the NEWEST would have done the opposite on both counts.
|
|
12937
|
+
*/
|
|
12938
|
+
function historicEntries(root) {
|
|
12939
|
+
const atRoot = `${(git(root, ["rev-parse", "--show-prefix"]) ?? "").trim()}${LEDGER}`;
|
|
12940
|
+
const commits = git(root, [
|
|
12941
|
+
"log",
|
|
12942
|
+
"--full-history",
|
|
12943
|
+
"--topo-order",
|
|
12944
|
+
"--format=%H",
|
|
12945
|
+
"--",
|
|
12946
|
+
LEDGER
|
|
12947
|
+
]);
|
|
12948
|
+
if (commits === null) return null;
|
|
12949
|
+
const seen = /* @__PURE__ */ new Map();
|
|
12950
|
+
for (const sha of commits.split("\n").filter((s) => s !== "")) {
|
|
12951
|
+
const text = git(root, ["show", `${sha}:${atRoot}`]);
|
|
12952
|
+
if (text === null) {
|
|
12953
|
+
const listed = git(root, [
|
|
12954
|
+
"ls-tree",
|
|
12955
|
+
"--full-tree",
|
|
12956
|
+
"--name-only",
|
|
12957
|
+
sha,
|
|
12958
|
+
"--",
|
|
12959
|
+
atRoot
|
|
12960
|
+
]);
|
|
12961
|
+
if (listed !== null && listed.trim() === "") continue;
|
|
12962
|
+
return null;
|
|
12963
|
+
}
|
|
12964
|
+
const where = sha.slice(0, 7);
|
|
12965
|
+
const parsed = parseLedger(text, LEDGER);
|
|
12966
|
+
if (parsed.ok) {
|
|
12967
|
+
for (const entry of parsed.ledger.entries) seen.set(entry.id, {
|
|
12968
|
+
id: entry.id,
|
|
12969
|
+
digest: entryDigest(entry),
|
|
12970
|
+
entry,
|
|
12971
|
+
where
|
|
12972
|
+
});
|
|
12973
|
+
continue;
|
|
12974
|
+
}
|
|
12975
|
+
for (const m of text.matchAll(/^\s*(?:-\s+)?id:\s*["']?([^\s"']+)/gm)) {
|
|
12976
|
+
const id = m[1] ?? "";
|
|
12977
|
+
if (!seen.has(id)) seen.set(id, {
|
|
12978
|
+
id,
|
|
12979
|
+
digest: null,
|
|
12980
|
+
where
|
|
12981
|
+
});
|
|
12982
|
+
}
|
|
12983
|
+
}
|
|
12984
|
+
return [...seen.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
12985
|
+
}
|
|
12986
|
+
/**
|
|
12854
12987
|
* The widening rule: a link, a `ksor.superseded_by` pointer or a companion
|
|
12855
12988
|
* body may reach a target whose audience contains `public` or contains every
|
|
12856
12989
|
* identifier in the source's — then every reader of the source can read the
|
|
@@ -13715,9 +13848,14 @@ function checkAgainstPolicy(concept, policy, refusals) {
|
|
|
13715
13848
|
}
|
|
13716
13849
|
}
|
|
13717
13850
|
//#endregion
|
|
13718
|
-
//#region ../content/dist/record-
|
|
13851
|
+
//#region ../content/dist/record-CKv4cZY8.mjs
|
|
13719
13852
|
const hex64 = string().regex(/^[0-9a-f]{64}$/, "a sha256 hex digest");
|
|
13720
13853
|
const viewerList = array(string().min(1));
|
|
13854
|
+
const bundleEntry = object({
|
|
13855
|
+
viewer: string().min(1),
|
|
13856
|
+
sha256: hex64,
|
|
13857
|
+
files: number().int().nonnegative()
|
|
13858
|
+
}).strict();
|
|
13721
13859
|
const lockSchema = object({
|
|
13722
13860
|
format: literal(1),
|
|
13723
13861
|
build_id: string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
@@ -13765,7 +13903,8 @@ const lockSchema = object({
|
|
|
13765
13903
|
indexes: array(object({
|
|
13766
13904
|
path: string().min(1),
|
|
13767
13905
|
sha256: hex64
|
|
13768
|
-
}).strict())
|
|
13906
|
+
}).strict()),
|
|
13907
|
+
bundles: array(bundleEntry)
|
|
13769
13908
|
}).strict();
|
|
13770
13909
|
function parseLock(text) {
|
|
13771
13910
|
let value;
|
|
@@ -13790,139 +13929,6 @@ function parseLock(text) {
|
|
|
13790
13929
|
lock: parsed.data
|
|
13791
13930
|
};
|
|
13792
13931
|
}
|
|
13793
|
-
/**
|
|
13794
|
-
* The takedown ledger's OTHER baseline: every entry any COMMITTED version of
|
|
13795
|
-
* `.ksor/takedowns.yaml` has ever carried.
|
|
13796
|
-
*
|
|
13797
|
-
* The committed lock is a baseline too, and a good one — it holds each entry's
|
|
13798
|
-
* digest, so an entry retargeted in place is caught. What it cannot do is prove
|
|
13799
|
-
* that an entry was never deleted, because the lock travels in the SAME change
|
|
13800
|
-
* as the ledger: delete the row, recompute `ledger_sha256`, empty
|
|
13801
|
-
* `ledger_entries`, and the two agree with each other about a denial that is
|
|
13802
|
-
* gone. Only history remembers.
|
|
13803
|
-
*
|
|
13804
|
-
* The baseline may be INCOMPLETE only if it SAYS so: every version history
|
|
13805
|
-
* holds is read, or `entries` comes back null and the caller reports that it
|
|
13806
|
-
* could not verify. A version silently skipped would contribute neither
|
|
13807
|
-
* digests nor ids while the answer still read "verified"
|
|
13808
|
-
* (`git-ledger.integration.test.ts`).
|
|
13809
|
-
*
|
|
13810
|
-
* This lives in the record module because THREE surfaces need the same answer —
|
|
13811
|
-
* `ksor build`, the emitted checker, and the site's stage (decision 19: a
|
|
13812
|
-
* surface that refuses must refuse on both surfaces). Plain `git log` / `git
|
|
13813
|
-
* show`, so nothing here needs installing.
|
|
13814
|
-
*/
|
|
13815
|
-
const LEDGER = ".ksor/takedowns.yaml";
|
|
13816
|
-
/**
|
|
13817
|
-
* `spawnSync` defaults to a 1 MB stdout buffer, and past it the child is KILLED
|
|
13818
|
-
* — `status` comes back null, so the query reads as a failure. A ledger with a
|
|
13819
|
-
* few thousand entries, or one entry carrying a long reason, clears 1 MB
|
|
13820
|
-
* easily, and the version was then dropped from the baseline while the caller
|
|
13821
|
-
* was still told history had been verified. The ceiling stays finite on
|
|
13822
|
-
* purpose: past it this returns null, which is a state the caller SAYS.
|
|
13823
|
-
*/
|
|
13824
|
-
const MAX_BUFFER = 67108864;
|
|
13825
|
-
/** One git query, read-only. Null on any non-zero exit, including no git at all. */
|
|
13826
|
-
function git(root, args) {
|
|
13827
|
-
const r = spawnSync("git", [...args], {
|
|
13828
|
-
cwd: root,
|
|
13829
|
-
encoding: "utf8",
|
|
13830
|
-
maxBuffer: MAX_BUFFER
|
|
13831
|
-
});
|
|
13832
|
-
return r.status === 0 ? r.stdout : null;
|
|
13833
|
-
}
|
|
13834
|
-
function historicLedger(root) {
|
|
13835
|
-
const inside = git(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
13836
|
-
if (inside === null || inside.trim() !== "true") return {
|
|
13837
|
-
repository: false,
|
|
13838
|
-
shallow: false,
|
|
13839
|
-
entries: null,
|
|
13840
|
-
unreadable: null
|
|
13841
|
-
};
|
|
13842
|
-
const shallow = (git(root, ["rev-parse", "--is-shallow-repository"]) ?? "").trim() === "true";
|
|
13843
|
-
const born = git(root, [
|
|
13844
|
-
"rev-parse",
|
|
13845
|
-
"--verify",
|
|
13846
|
-
"--quiet",
|
|
13847
|
-
"HEAD"
|
|
13848
|
-
]) !== null;
|
|
13849
|
-
const entries = shallow ? null : born ? historicEntries(root) : [];
|
|
13850
|
-
return {
|
|
13851
|
-
repository: true,
|
|
13852
|
-
shallow,
|
|
13853
|
-
entries,
|
|
13854
|
-
unreadable: entries !== null ? null : shallow ? "shallow" : "unreadable"
|
|
13855
|
-
};
|
|
13856
|
-
}
|
|
13857
|
-
/**
|
|
13858
|
-
* Every id history has ever recorded, each with the text it carried the FIRST
|
|
13859
|
-
* time it was written. A version that parses contributes each entry's digest,
|
|
13860
|
-
* so an entry EDITED in place is caught, not only one deleted; a version that
|
|
13861
|
-
* no longer parses still contributes its ids, read permissively — the point
|
|
13862
|
-
* there is that an id once written never disappears.
|
|
13863
|
-
*
|
|
13864
|
-
* FIRST, not every: keying this by `id\tdigest` kept one baseline entry per
|
|
13865
|
-
* version an id ever had, so a tamper that was COMMITTED and then UNDONE left
|
|
13866
|
-
* two digests for one id, the restored entry matched only one of them, and
|
|
13867
|
-
* `ksor-ledger-amended` fired for good. The record became permanently
|
|
13868
|
-
* unbuildable — by a tamper that had already been put right — and the only
|
|
13869
|
-
* escape was rewriting git history, which is not a remedy a refusal may
|
|
13870
|
-
* demand (found in review, 2026-08-25).
|
|
13871
|
-
*
|
|
13872
|
-
* Taking the OLDEST is what makes the guarantee both enforceable and
|
|
13873
|
-
* escapable. It still refuses a committed tamper (the baseline is what the
|
|
13874
|
-
* entry said when it was written, so committing the edit does not launder
|
|
13875
|
-
* it), and the remedy it names — put the entry back — now actually clears
|
|
13876
|
-
* it. Taking the NEWEST would have done the opposite on both counts.
|
|
13877
|
-
*/
|
|
13878
|
-
function historicEntries(root) {
|
|
13879
|
-
const atRoot = `${(git(root, ["rev-parse", "--show-prefix"]) ?? "").trim()}${LEDGER}`;
|
|
13880
|
-
const commits = git(root, [
|
|
13881
|
-
"log",
|
|
13882
|
-
"--full-history",
|
|
13883
|
-
"--topo-order",
|
|
13884
|
-
"--format=%H",
|
|
13885
|
-
"--",
|
|
13886
|
-
LEDGER
|
|
13887
|
-
]);
|
|
13888
|
-
if (commits === null) return null;
|
|
13889
|
-
const seen = /* @__PURE__ */ new Map();
|
|
13890
|
-
for (const sha of commits.split("\n").filter((s) => s !== "")) {
|
|
13891
|
-
const text = git(root, ["show", `${sha}:${atRoot}`]);
|
|
13892
|
-
if (text === null) {
|
|
13893
|
-
const listed = git(root, [
|
|
13894
|
-
"ls-tree",
|
|
13895
|
-
"--full-tree",
|
|
13896
|
-
"--name-only",
|
|
13897
|
-
sha,
|
|
13898
|
-
"--",
|
|
13899
|
-
atRoot
|
|
13900
|
-
]);
|
|
13901
|
-
if (listed !== null && listed.trim() === "") continue;
|
|
13902
|
-
return null;
|
|
13903
|
-
}
|
|
13904
|
-
const where = sha.slice(0, 7);
|
|
13905
|
-
const parsed = parseLedger(text, LEDGER);
|
|
13906
|
-
if (parsed.ok) {
|
|
13907
|
-
for (const entry of parsed.ledger.entries) seen.set(entry.id, {
|
|
13908
|
-
id: entry.id,
|
|
13909
|
-
digest: entryDigest(entry),
|
|
13910
|
-
entry,
|
|
13911
|
-
where
|
|
13912
|
-
});
|
|
13913
|
-
continue;
|
|
13914
|
-
}
|
|
13915
|
-
for (const m of text.matchAll(/^\s*(?:-\s+)?id:\s*["']?([^\s"']+)/gm)) {
|
|
13916
|
-
const id = m[1] ?? "";
|
|
13917
|
-
if (!seen.has(id)) seen.set(id, {
|
|
13918
|
-
id,
|
|
13919
|
-
digest: null,
|
|
13920
|
-
where
|
|
13921
|
-
});
|
|
13922
|
-
}
|
|
13923
|
-
}
|
|
13924
|
-
return [...seen.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
13925
|
-
}
|
|
13926
13932
|
//#endregion
|
|
13927
13933
|
//#region src/build/git.ts
|
|
13928
13934
|
/** The inputs a projection reads; nothing else moves `source_commit` (the lock itself included). */
|