@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 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 its own tutorial.
52
+ to **[Serve it — with a floor](https://github.com/panaversity/ksor/blob/main/docs/tutorials/04-serve-it.md)**.
53
53
 
54
- The three so far, in reading order — pick by what you want from it:
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 — Node only, nothing else |
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), as does
75
- > `build --bundles`.
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-6fB_kR4G.mjs
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-Cxw0SUG3.mjs
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). */