@panaversity/ksor 0.0.58 → 0.0.59

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,172 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.59
4
+
5
+ ### Patch Changes
6
+
7
+ - 4d7dad7: The agent tier authenticates through `claude`'s own login, never an API key.
8
+
9
+ Where the repository needs model inference — today, the skill evals — it uses
10
+ `claude -p`: a developer's logged-in CLI locally, and in CI a long-lived token
11
+ from `claude setup-token` in `CLAUDE_CODE_OAUTH_TOKEN`. The tier no longer reads
12
+ `ANTHROPIC_API_KEY` and no longer passes `--bare`, because bare mode does not
13
+ read the OAuth token (per the official headless docs) and would have quietly
14
+ fallen back to needing the key it is not supposed to have. A guard holds both.
15
+
16
+ The pending owner action changes with it: a `CLAUDE_CODE_OAUTH_TOKEN` repository
17
+ secret, not an API key. Test infrastructure only; nothing an adopter installs
18
+ behaves differently.
19
+
20
+ - 5bf3733: `ksor build --bundles` is implemented (issue #158). It used to parse the flag
21
+ and exit `2`; it now runs an ordinary build and additionally writes one OKF
22
+ bundle per viewer under `.ksor/out/bundles/<viewer>/` — `public`, and
23
+ `[public, X]` for each audience X your `.ksor/governance.yaml` registers. A
24
+ bundle holds exactly what that viewer's machine surfaces publish: the admitted
25
+ concepts (stable, effective, unexpired, not taken down, audience overlapping),
26
+ their companions, the assets their bodies reference, and every `index.md`
27
+ regenerated for that filtered tree with `okf_version` at the root. No byte of a
28
+ concept excluded for AUDIENCE reaches it — not a title, a path, a description
29
+ or an asset — because the record checker refuses a link that widens audience
30
+ before a bundle is planned. A document excluded for a lifecycle or ledger
31
+ reason is different, and deliberately: bodies are copied verbatim, never
32
+ rewritten, so a link to a draft or a taken-down document keeps that path and
33
+ the build reports the dangling link instead of editing your prose. Any OKF
34
+ consumer reads a bundle with no ksor in the loop. The directory is replaced on
35
+ every run, a copy of `build.lock.json` is written beside the bundles, and the
36
+ scaffold's `.ksor/*` rule already gitignores it.
37
+
38
+ `build.lock.json` gains `bundles[]` — one `{ viewer, sha256, files }` per
39
+ viewer, recorded on EVERY build whether or not the flag was passed, so a
40
+ bundle directory can be matched to the publication that produced it. The
41
+ digest is sha256 over the JSON of the bundle's sorted `[path, sha256]` pairs.
42
+ `build_id` is unchanged: the bundles are a function of what it already
43
+ hashes.
44
+
45
+ **Upgrading:** a lock written by an earlier ksor lacks the key, so `ksor build`
46
+ refuses it as `ksor-lock-invalid` and says to delete it. It does NOT regenerate
47
+ one it cannot read — the lock is also a takedown baseline, and a baseline
48
+ nothing can parse is one that quietly holds nothing. So the first `pnpm build`
49
+ after upgrading (`ksor build && <site build>`, which is the deploy command too)
50
+ is red until the stale `build.lock.json` is gone. `ksor migrate` now offers that
51
+ deletion like any other change it carries, so the four steps in
52
+ `docs/upgrading.md` cover it; deleting the file by hand and rebuilding does the
53
+ same thing.
54
+
55
+ Two new refusals, both raised on EVERY build — not only under `--bundles` —
56
+ because the lock lists `bundles[]` either way and a digest for a directory the
57
+ tool refuses to write would be a provenance claim about something that cannot
58
+ exist. `ksor-audience-identifier-invalid`: a registered audience that cannot
59
+ name a bundle directory (`../x`, `.hidden`, `-x`, `internal.`, or
60
+ `build.lock.json`, whose name the lock copy beside the bundles already holds) —
61
+ an identifier must start with a letter or a digit, may then use letters, digits,
62
+ `-`, `_` and `.`, and may not END in `.`, which Windows strips from a path
63
+ segment, so `internal.` and `internal` would be one directory there.
64
+ `ksor-audience-identifier-collides`: two registered audiences differing only in
65
+ case, such as `internal` and `Internal`, which are two viewers in your policy
66
+ and ONE directory on macOS and Windows — the second bundle would merge into the
67
+ first, leaving a directory that holds concepts its viewer may not read.
68
+
69
+ - d896f67: The skill harness gains two fixtures a baseline plausibly gets wrong, and is
70
+ parametrised over a CASES table. Nothing an adopter installs changes: the
71
+ fixtures, the graders and the baseline live under `src/evals/`, outside the
72
+ tarball.
73
+
74
+ Three armed runs on the clean two-page PDF had shown both arms — the agent with
75
+ `add-sources` and the agent without it — passing every deterministic gate; a
76
+ harness that cannot tell the arms apart is measuring the fixture, not the
77
+ skill. So `expense-policy-hard.pdf` is built out of the acts a careless
78
+ conversion fails a deterministic gate on: a five-row per-diem table (a dropped
79
+ row is a value `verify.mjs` cannot see missing), the payment window stated
80
+ twice and differently — "10 working days" in §2, "ten (10) business days" in §5
81
+ — which the skill says stays two statements, flagged; `1,250` beside `1.250` on
82
+ one row, where a misread makes them equal and a substring check cannot tell; a
83
+ threshold with a thousands separator; and a running footer on both pages.
84
+ `scanned-policy.pdf` is the same policy rasterised — two pages of picture, no
85
+ text layer — for which the skill's instruction is to stop and tell the owner.
86
+
87
+ `skill-cases.ts` is the table: fixture, prompt, the outcome class a correct run
88
+ belongs to (`converted` or `refused`), and the gates that apply. Its body
89
+ gates are pure functions, so a unit suite mutation-tests each one — smoothing
90
+ the two statements, misreading the figure, dropping a row, dropping or
91
+ misreading a thousands separator, keeping the footer, inventing a currency —
92
+ and watches exactly the gate built for it go red. The agent suite also asserts
93
+ the fixtures are what they claim: each committed `.txt` is its PDF's
94
+ `pdftotext -layout` extraction, and the scanned PDF yields only whitespace. For
95
+ the scanned case the WITH arm's correct outcome — wrote nothing, named the
96
+ missing text layer — is the pass; what the baseline did is reported.
97
+
98
+ Not run here: the armed arms. `SKILL_BASELINE` carries no row for either new
99
+ fixture; the hard fixture's first armed run is pending and its row lands with
100
+ the run that produces it.
101
+
102
+ RESULTS
103
+
104
+ - 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.
105
+
106
+ 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.
107
+
108
+ - db39c11: Switch Next.js telemetry off in the scaffold, and make the README describe the
109
+ scaffold that ships.
110
+
111
+ **Nothing phones home — now true of the artifact, not only of the pitch.**
112
+ Next.js ships with anonymous usage telemetry ON, so every site `ksor init`
113
+ emitted posted to `telemetry.nextjs.org` on its first `build` and `dev` while
114
+ the README promised nothing phones home. The site's two scripts now run next
115
+ through `system/site/next-no-telemetry.mjs`, a dependency-free wrapper that
116
+ hands it an environment carrying `NEXT_TELEMETRY_DISABLED=1`; that is where
117
+ every package manager's root script and the deploy's `pnpm build` end up, and
118
+ `.env.example` says so. It is the process environment rather than
119
+ `next.config.mjs` on purpose: read in 16.3.3's source, `next build` constructs
120
+ its telemetry after loading the config, but the `next dev` parent process never
121
+ loads the config and still records a session event on exit — a config-time
122
+ assignment would have left that one in place with nothing red. And it is a file
123
+ rather than a `NEXT_TELEMETRY_DISABLED=1 next …` prefix in `package.json`
124
+ because that prefix is POSIX shell syntax: cmd.exe refuses it under pnpm and
125
+ npm on Windows, a platform CI walks `ksor init` on, so the prefix would have
126
+ traded a quiet leak for a loud break. An existing scaffold gets the switch from
127
+ `ksor migrate --write-site`, which offers the wrapper and the two scripts as a
128
+ diff.
129
+
130
+ **The README no longer contradicts the tree.** Five sentences described a
131
+ product that does not ship, each now corrected and held by a test against the
132
+ thing that decides it: `pnpm serve` is `ksor serve` alone (provisioning is
133
+ `pnpm provision`, publishing is `pnpm refresh`, and ingest reports `unchanged`
134
+ only when documents, governance, toolchain AND source commit all match the
135
+ serving generation); the gate paragraph names the browser, Postgres, Windows,
136
+ npm/bun and container acceptance CI has run for weeks instead of promising
137
+ them, two-way against the workflow's job names; an authored `log.md` is refused
138
+ (`ksor-reserved-name`) and never generated, while `index.md` is generated by
139
+ `ksor build` and drift-checked; the hello-world tutorial row says Part 2 needs a
140
+ free Postgres and a free embedding key (the package README's copy too); and the
141
+ project-structure tree is labelled abridged, with every path it names asserted
142
+ to exist in a scaffold freshly emitted by the built CLI.
143
+
144
+ - 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.
145
+
146
+ - **`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.
147
+ - **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.
148
+ - **`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.
149
+ - **`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`.
150
+ - **`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.
151
+
152
+ - babf416: Five tests now hold what they claimed. Nothing served changes; what changes is what a green run proves.
153
+
154
+ - **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.
155
+ - **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.
156
+ - **`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.
157
+ - **`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.
158
+ - **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.
159
+
160
+ - 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.
161
+
162
+ 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.
163
+
164
+ - 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.
165
+
166
+ 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.
167
+
168
+ **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.
169
+
3
170
  ## 0.0.58
4
171
 
5
172
  ### 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). */