diff --git a/okf/DEVIATIONS.md b/okf/DEVIATIONS.md index ea2144a..5e754e1 100644 --- a/okf/DEVIATIONS.md +++ b/okf/DEVIATIONS.md @@ -179,10 +179,10 @@ the body survives and `_rev` re-derives (§14: a former edit's revHash changes, the body text does not). Once a bundle has cycled once, every body is already `base:null`, so further cycles preserve even `_rev`. -**okf §10 `compact` injects the `@auto-okf/store` `initVault` rather than +**okf §10 `compact` injects the `@auto-okf/store` `Bundle.init` rather than importing it.** Consistent with the rest of `@auto-okf/faces` ("the substrate is injected — nothing here imports the substrate"), `compact(vault, opts)` -takes `opts.initVault` (the store factory) instead of adding a hard +takes `opts.initBundle` (the store factory) instead of adding a hard `@auto-okf/store` dependency to the faces package. The CLI (`okf/cli`) wires the two together. The predecessor is never deleted by compact — its path is returned for the operator to archive/destroy out-of-band (§10 step 5). diff --git a/okf/README.md b/okf/README.md index 4d4a084..7261ebd 100644 --- a/okf/README.md +++ b/okf/README.md @@ -6,16 +6,16 @@ A multi-writer knowledge bundle on markdown, agents and humans co-writing one vault. ```js -import { initVault } from '@auto-okf/store' +import { Bundle } from '@auto-okf/store' import { materialize } from '@auto-okf/faces' -const vault = await initVault('./vault') // the op log is the source of truth -const { tag: id } = await vault.createConcept('guides/getting-started', 'guide') -await vault.setField(id, 'title', 'Getting started') -await vault.setBody(id, null, 'Read the [architecture](/guides/architecture) first.') -await vault.update() -await materialize(vault, './bundle') // guides/getting-started.md + index.md + log.md -await vault.close() +const bundle = await Bundle.init('./storage') // the op log is the source of truth +const { tag: id } = await bundle.createConcept('guides/getting-started', 'guide') +await bundle.setField(id, 'title', 'Getting started') +await bundle.setBody(id, null, 'Read the [architecture](/guides/architecture) first.') +await bundle.update() +await materialize(bundle, './bundle') // guides/getting-started.md + index.md + log.md +await bundle.close() ``` Open `./bundle` in any markdown reader and it is a plain OKF bundle; diff --git a/okf/cli/README.md b/okf/cli/README.md index c33d46a..6e420f7 100644 --- a/okf/cli/README.md +++ b/okf/cli/README.md @@ -79,6 +79,19 @@ pnpm add @auto-okf/cli # or run the repo-local bin: okf/cli/bin/auto-okf.js There is no content-write command: content flows in through `ingest`/`watch` (edit the markdown) or through the `@auto-okf/store` API (agents). +## Verifying content is in the log + +A vault has no privileged bundle directory; the emission manifest +(`.okf-manifest.json`) lives in each target directory, so materializing to +a scratch directory never disturbs another bundle: + +```sh +$ mkdir ./scratch && auto-okf materialize ./vault ./scratch +``` + +An empty scratch dir that fills on materialize is proof the content is in +the log — everything that appears came from it. + ## Exit codes `0` success · `1` operational error (including a governance op that apply diff --git a/okf/cli/bin/auto-okf.js b/okf/cli/bin/auto-okf.js old mode 100644 new mode 100755 diff --git a/okf/cli/lib/run.js b/okf/cli/lib/run.js index 44b5fe1..57239d3 100644 --- a/okf/cli/lib/run.js +++ b/okf/cli/lib/run.js @@ -19,7 +19,7 @@ import { join } from 'node:path' import b4a from 'b4a' import { command, flag, arg, summary, header, footer, bail, validate } from 'paparam' import { keyspace as K } from '@auto-okf/core' -import { initVault, openVault, joinVault, METADATA_FILE } from '@auto-okf/store' +import { Bundle, METADATA_FILE } from '@auto-okf/store' import { materialize, createIngestWatcher, wanted, compact } from '@auto-okf/faces' const TITLE = 'auto-okf — multi-writer OKF bundles on autobee (okf SPEC §13)' @@ -295,7 +295,7 @@ async function withVault(dir, opts, fn) { `'${dir}' is not a vault (no ${METADATA_FILE}); run \`auto-okf init\` or \`auto-okf join\` first` ) } - const vault = await openVault(dir, opts) + const vault = await Bundle.open(dir, opts) try { return await fn(vault) } finally { @@ -329,7 +329,7 @@ async function cmdInit({ args, flags }) { if (flags.encryptionKey) { opts.encryptionKey = hexKey(flags.encryptionKey, '--encryption-key') } - const vault = await initVault(dir, opts) + const vault = await Bundle.init(dir, opts) try { printIdentity(vault) console.log(`storage: ${dir}`) @@ -356,8 +356,8 @@ async function cmdJoin({ args, flags }) { throw new UsageError('a vault key is required to join fresh storage') } const vault = pinned && key === undefined - ? await openVault(dir, opts) - : await joinVault(dir, hexKey(key, 'vault key'), opts) + ? await Bundle.open(dir, opts) + : await Bundle.join(dir, hexKey(key, 'vault key'), opts) try { printIdentity(vault) console.log('hand the writer key to an indexer (`auto-okf add-writer`), then replicate') @@ -469,7 +469,7 @@ async function cmdWatch({ args, flags }) { `'${dir}' is not a vault (no ${METADATA_FILE}); run \`auto-okf init\` or \`auto-okf join\` first` ) } - const vault = await openVault(dir) + const vault = await Bundle.open(dir) let watcher = null let swarmed = false try { @@ -545,7 +545,7 @@ async function cmdCompact({ args, flags }) { const faceDir = flags.face || `${outDir}.face` return withVault(dir, {}, async (vault) => { const res = await compact(vault, { - initVault, + initBundle: Bundle.init, toDir: faceDir, vaultDir: outDir, force: flags.force === true diff --git a/okf/docs/decisions/ADR-001-path-scoped-ingest.md b/okf/docs/decisions/ADR-001-path-scoped-ingest.md new file mode 100644 index 0000000..ff7e247 --- /dev/null +++ b/okf/docs/decisions/ADR-001-path-scoped-ingest.md @@ -0,0 +1,62 @@ +# ADR-001: Path-scoped ingest — `ingest [paths…]` + +## Status +Accepted + +## Date +2026-07-31 + +## Context +From the six-writer confusion experiment (AGENT-CONFUSION-LOG.md, finding +F1; log kept outside this repo at +`0x/lpmq/0/elements-of-documentation/`): the CLI's only ingest form scans +the entire bundle. Under concurrent writers this produced a complete +three-perspective incident — one agent refused the command ("to avoid +folding other agents' possibly half-written pending files") and dropped +to the store API; a second ran it and ingested a third agent's pending +file as a side effect ("If the-clerk's file had been mid-write, I would +have ingested a partial draft"); the third then saw its own ingest report +`0 op(s) from 0 file(s)` (see ADR-003). + +The concurrent-writer case is in-scope by design: the flagship example is +four agents on one vault. + +## Decision +Accept optional trailing bundle-relative paths (or globs) that restrict +the ingest scan set: + +```sh +auto-okf ingest ./vault ./bundle drafts/mine.md # only this file +auto-okf ingest ./vault ./bundle # unchanged: full scan +``` + +A named path that does not exist, or that resolves outside the bundle +root, is an error (exit 1, path named, no ops appended). The zero-path +form is byte-identical in behavior to today — extension, not +modification. + +## Alternatives Considered + +### Per-file claim/lock protocol in the bundle +Rejected. The experiment recorded zero corruption and zero lock errors +across ~17 concurrent invocations (F6); the failure mode is *selection +and reporting*, not safety. Locks import the coordination complexity the +log exists to avoid. + +### Make per-file ingest the default / refuse multi-file sweeps +Rejected. Breaks the documented single-human workflow (edit many files, +ingest once). + +## Consequences +- SPEC §9: the ingest scan-set definition gains "restricted to the + argument set when paths are given." §13: CLI surface line updated. +- Agents get concurrent-writer politeness without leaving the CLI (the + workaround today is the store API's `ingestFile`, which one agent had + to discover unaided). + +## Acceptance +- [ ] Two files pending; path-scoped ingest of one emits ops for that + file only; the other remains pending-ingest. +- [ ] Path outside the bundle root → exit 1, zero ops appended. +- [ ] Zero-path invocation unchanged (existing invariant suite passes + untouched). diff --git a/okf/docs/decisions/ADR-002-status-command.md b/okf/docs/decisions/ADR-002-status-command.md new file mode 100644 index 0000000..f386d14 --- /dev/null +++ b/okf/docs/decisions/ADR-002-status-command.md @@ -0,0 +1,67 @@ +# ADR-002: `status` — per-file vault/bundle state porcelain + +## Status +Accepted + +## Date +2026-07-31 + +## Context +The strongest cross-agent signal in the confusion experiment (findings +F1/F5): three of six first-contact agents read this repo's *source* to +answer what are all per-file state questions. One diagnosed a +`0 op(s) from 0 file(s)` result by reading `okf/faces/lib/ingest.js` +("the wording gave no hint the file was already ingested vs. never +seen"); another grepped `fsatomic.js` to learn the emission manifest is +per-directory; a third read docs mid-flight to confirm materialize would +not clobber a foreign pending file. Agents source-dive where the surface +is ambiguous; humans mostly won't. + +## Decision +Add a read-only command: + +```sh +auto-okf status [--json] +``` + +One line per file, one state from a fixed vocabulary: + +| state | meaning | +| --- | --- | +| `in-log` | on-disk bytes match the log's canonical projection (hash match) | +| `pending-ingest` | hand-authored/edited; not yet in the log | +| `dirty` | in the log but on-disk bytes diverge from the projection | +| `absent` | in the log, missing from disk (materialize would restore) | +| `foreign` | not in the log, not manifest-tracked (e.g. another writer's WIP) | + +`--json` emits the same as a machine-readable array. Strictly read-only: +appends no ops, writes no files, touches no manifest. + +## Alternatives Considered + +### Fold flag-queue reporting into `status` +Deferred (open question Q3 in the proposals doc). `flags` already +exists; one command, one question. + +### Richer prose messages on existing commands instead +Insufficient alone — messages answer the question for the command you +ran; `status` answers it before you run anything (and ADR-003 improves +the message anyway). + +## Consequences +- The state vocabulary becomes contract (Hyrum's law applies to the + `--json` schema especially — it will be the most machine-depended-on + surface in the CLI; review it with API-design care, once). +- SPEC §9 names the state vocabulary (it exists implicitly in the + ingest/materialize rules today); §13 adds the command. +- The scratch-materialize verification idiom four agents independently + invented becomes unnecessary for state questions (still valid for + proof-from-log; see ADR-007). + +## Acceptance +- [ ] Reproduce the swept-writer incident: write a file, have a second + process ingest+materialize it, run `status` → `in-log`, not + `pending-ingest`. +- [ ] A foreign mid-write file reports `foreign` and is not ingested. +- [ ] `--json` parses and is identical across two consecutive runs with + no intervening ops. diff --git a/okf/docs/decisions/ADR-003-ingest-report-split.md b/okf/docs/decisions/ADR-003-ingest-report-split.md new file mode 100644 index 0000000..e710bee --- /dev/null +++ b/okf/docs/decisions/ADR-003-ingest-report-split.md @@ -0,0 +1,48 @@ +# ADR-003: Ingest report distinguishes "already in log" from "nothing found" + +## Status +Accepted + +## Date +2026-07-31 + +## Context +Confusion finding F1 (victim side): `ingest complete: 0 op(s) from 0 +file(s)` is emitted both when the bundle has no candidates and when every +candidate is already converged (hash match against the emission +manifest). An agent whose brand-new file had been swept into the log by a +concurrent writer's whole-bundle ingest received this message and "had to +read source to distinguish" already-ingested from never-seen — the +scanner computes the hash-match skips either way; it just doesn't say so. + +## Decision +Report the skip count: + +``` +ingest complete: 0 op(s) from 0 file(s); 7 file(s) already up to date +``` + +Message-text change only; exit codes untouched. Land now while the +message is young — the packages are unpublished, so external +message-parsing dependents are ~nil, and ADR-002's `--json` is the +durable machine answer regardless. + +## Alternatives Considered + +### `--json` on ingest instead of rewording +Not instead — the human/log-reading case deserves the plain sentence; +structured output arrives with ADR-002 where the evidence is. + +### Leave it to `status` (ADR-002) +`status` answers the question when you think to ask it; the ingest +report is what you are already looking at when the confusion strikes. + +## Consequences +- SPEC §9 ingest-reporting sentence updated. +- The one-line report becomes slightly longer only when skips occurred. + +## Acceptance +- [ ] The swept-writer scenario re-run prints a nonzero + "already up to date" count. +- [ ] An empty bundle prints no skip clause (or "0", decided once, + consistently). diff --git a/okf/docs/decisions/ADR-004-materialize-exit-codes.md b/okf/docs/decisions/ADR-004-materialize-exit-codes.md new file mode 100644 index 0000000..e81f77a --- /dev/null +++ b/okf/docs/decisions/ADR-004-materialize-exit-codes.md @@ -0,0 +1,63 @@ +# ADR-004: Materialize exit-code taxonomy — 0 clean / 3 pending-only / 1 failure + +## Status +Accepted + +## Date +2026-07-31 + +## Context +Confusion finding F3: a materialize that wrote everything the caller +owned and correctly left a concurrent writer's pending file in place +exited 1 — the agent logged that it "momentarily looked like a failure." +Under multi-writer use, a *normal* state (someone else mid-authoring) +surfaces as failure on a *healthy* operation. + +The implementation is a single line — `return report.ok ? 0 : 1` +(`okf/cli/lib/run.js`, the N3 comment) — and the amendment window was +verified open (proposals doc Q2, resolved 2026-07-31): + +- Library tests (`okf/test/faces-preservation.test.js`) assert on the + `report.pendingIngest` object, not the exit code — unaffected. +- The CLI test asserts exit 1 only for "not a vault" (real failure) and + exit 0 for clean materialize — both preserved. +- No test or harness code anywhere pins exit 1 for the pending case. +- The packages are unpublished; external Hyrum exposure is ~nil. + +## Decision +Three named outcomes, in the SPEC: + +| exit | meaning | +| --- | --- | +| 0 | projected everything; no pending files encountered | +| 3 | projected everything owned; N pending-ingest file(s) left in place — stderr lists them prefixed `notice:`, not `error:` | +| 1 | real failure (I/O, corrupt vault, bad args) | + +Scripts treating any nonzero as failure degrade gracefully (exit 3 is +still nonzero — today's semantics). The `unsafe` and `stale` report +lists must also be placed in the taxonomy: `stale` (left in place, per +spec) accompanies exit 0/3 as a notice; `unsafe` paths (a §9 defense) +stay on the failure side unless the SPEC review concludes otherwise — +decided in this ADR's spec delta, once. + +## Alternatives Considered + +### Exit 0 + notice for pending +Reads best for agents, but silently un-fails any pipeline that *relied* +on the stop. Rejected as the riskier flip; exit 3 keeps the stop while +naming the state. + +### Leave exit 1, document harder +The Auditor agent had read the docs and still flinched. An exit code is +the API here; prose can't fix a conflated code. + +## Consequences +- SPEC §9 (N3 behavior) + §13 updated; CHANGELOG line for the 1 → 3 + narrowing. +- One new CLI test pins each of the three codes (the pending case has no + test today — this ADR adds the missing one). + +## Acceptance +- [ ] Foreign-pending scenario exits 3 with `notice:` stderr lines. +- [ ] "Not a vault" and I/O failure still exit 1. +- [ ] Clean run still exits 0. diff --git a/okf/docs/decisions/ADR-005-oversize-preflight.md b/okf/docs/decisions/ADR-005-oversize-preflight.md new file mode 100644 index 0000000..ec0632c --- /dev/null +++ b/okf/docs/decisions/ADR-005-oversize-preflight.md @@ -0,0 +1,55 @@ +# ADR-005: Ingest preflights the op-size limit; the limit joins the CLI docs + +## Status +Accepted + +## Date +2026-07-31 + +## Context +Confusion experiment, Clerk log #3: the 65,536-byte `maxOpBytes` bound on +a `set-body` op is discoverable only by reading `okf/core` source. One +agent grepped for it proactively and sized its 41KB document against it; +an agent that didn't would have learned about the limit from the +apply-side `oversize-op` rejection — after authoring, at the wrong layer, +with a flag-queue entry as the error message. + +Finding F2 established the pattern that works: a counterintuitive +constraint stated plainly in the CLI README ("there is NO content-write +command") generated zero failed attempts across six first-contact agents. +This is the same treatment for the next constraint agents will hit. + +## Decision +Two parts: + +1. **Preflight in ingest (face-side):** a file whose body would exceed + `maxOpBytes` is reported by name, with the limit and the actual size, + before any op is appended — + `error: drafts/big.md body is 71,204 bytes; maxOpBytes is 65,536` — + exit 1, other files unaffected. +2. **One sentence in `okf/cli/README.md`:** the limit exists, its value, + and the remedy (split the document). + +The apply-side `oversize-op` defense stays untouched — validate at the +boundary, defend in depth. + +## Alternatives Considered + +### Raise or chunk `maxOpBytes` +Out of scope; the limit is a log-layer design decision with its own +rationale. This ADR is about *when the writer learns of it*. + +### Doc-only (no preflight) +Docs prevent the first-contact case, but a generated document can exceed +the limit at runtime; the preflight names the file at the moment of +failure instead of deferring to the flag queue. + +## Consequences +- SPEC §9 ingest rules gain the preflight check. +- Ingest gains one stat-and-compare per candidate file (negligible). + +## Acceptance +- [ ] Oversize file → named preflight error, exit 1, zero ops appended, + sibling files in the same run unaffected. +- [ ] A file at exactly the limit minus op overhead ingests. +- [ ] CLI README states the limit and remedy. diff --git a/okf/docs/decisions/ADR-006-wrong-tool-guardrail.md b/okf/docs/decisions/ADR-006-wrong-tool-guardrail.md new file mode 100644 index 0000000..27bed91 --- /dev/null +++ b/okf/docs/decisions/ADR-006-wrong-tool-guardrail.md @@ -0,0 +1,51 @@ +# ADR-006: Twin CLIs refuse each other's vaults by marker file + +## Status +Accepted + +## Date +2026-07-31 + +## Context +Confusion finding F4: across the six-agent experiment, zero cross-system +invocations occurred between `auto-okf` and `autovault-ld` — but the log +attributes that to five of six agents *finding and reading* +`vault/okf-vault.json`, and one agent stated the counterfactual: "Had +that marker file not existed I would likely have had to guess." The +repo ships two near-identical CLIs over the same substrate; the marker +is currently informational, not enforced. The experiment's luckiest +non-failure should become a guarantee. + +## Decision +Each CLI checks for the sibling system's marker on vault open and refuses +pointedly, naming the right tool: + +``` +error: ./vault is an auto-okf vault (okf-vault.json present); +use auto-okf, not autovault-ld +``` + +Symmetric: `auto-okf` refuses vault-ld storage identically. One stat plus +one read on the open path; exit 1; storage untouched. + +## Alternatives Considered + +### Auto-detect and delegate to the right CLI +Rejected: silent delegation hides which contract (and which SPEC) the +caller is under. Make the wrong thing hard and the right thing findable — +the error message does the routing. + +### Rely on the marker file being read (status quo) +That is the luck this ADR removes. + +## Consequences +- The one cross-system change in this batch: okf §13 and the vault-ld + SPEC's CLI section update together. +- Requires the marker-file names of both systems to be stable contract + (they already are in practice). + +## Acceptance +- [ ] `autovault-ld ` against an auto-okf vault → the refusal, exit + 1, storage untouched. +- [ ] `auto-okf ` against a vault-ld vault → symmetric refusal. +- [ ] Both CLIs against their own vaults: unchanged. diff --git a/okf/docs/decisions/ADR-007-doc-pack.md b/okf/docs/decisions/ADR-007-doc-pack.md new file mode 100644 index 0000000..69d9c8a --- /dev/null +++ b/okf/docs/decisions/ADR-007-doc-pack.md @@ -0,0 +1,63 @@ +# ADR-007: First-contact documentation pack (no behavior change) + +## Status +Accepted + +## Date +2026-07-31 + +## Context +Three findings from the confusion experiment are pure documentation gaps, +each measured by what first-contact agents did instead of reading a +sentence that didn't exist (F5, Stylist #1, Engineer #1, plus an +orchestrator observation): three independent source-dives to confirm the +emission manifest is per-directory; a store README headline snippet that +uses `initVault` where reopen is meant ("had I copied the headline +snippet verbatim I might have hit a refusal or worse"); a vestigial +top-level `packages/` directory that presents a second plausible bin +location; and an asymmetric workspace `.bin` (only `autovault-ld` +linked). + +Counter-evidence that docs carry weight here (F2): the README's explicit +"there is NO content-write command" pre-empted the same wrong guess in +four of six agents — zero failed attempts. + +## Decision +Four changes, no code behavior touched: + +1. **Manifest scoping sentence** in `okf/faces` and `okf/cli` READMEs: + "A vault has no privileged bundle directory; the emission manifest + (`.okf-manifest.json`) lives in each target directory, so + materializing to a scratch directory never disturbs another bundle." + Under a heading that blesses the idiom four agents invented + independently: *Verifying content is in the log* (materialize to an + empty scratch dir; everything that appears came from the log). +2. ~~**`initVault` vs `openVault`** in the store README headline + snippet.~~ Superseded by ADR-008: maintainer review identified the + snippet as a symptom of redundant wrapper vocabulary — the fix is + deleting the `initVault`/`openVault`/`joinVault` wrappers in favor of + the `Vault.init`/`Vault.open`/`Vault.join` statics both systems + already share. The README snippets get rewritten as part of that + sweep, not as a doc patch. +3. **Delete top-level `packages/`** — verified vestigial 2026-07-30: + empty scaffold dirs plus stray `node_modules`, no `package.json`, + outside the workspace globs (`vault-ld/*`, `okf/*`). +4. **Workspace `.bin` symmetry:** link `auto-okf` alongside + `autovault-ld`, or neither. + +## Alternatives Considered + +### Fold into each feature ADR +These are shippable today with zero risk; batching them behind feature +review would delay the cheapest wins. + +## Consequences +- First-contact agents stop source-diving for F5-class questions. +- Deleting `packages/` removes a wrong-bin trap (Engineer: "a less + careful reader could grab the wrong bin"). + +## Acceptance +- [ ] A re-run of the six-agent experiment produces zero source-dives + for manifest-scoping questions. +- [ ] `packages/` gone; workspace install and both test suites green. +- [ ] Both bins resolvable the same way (or documented identically). diff --git a/okf/docs/decisions/ADR-008-vault-lifecycle-api.md b/okf/docs/decisions/ADR-008-vault-lifecycle-api.md new file mode 100644 index 0000000..ce9b59b --- /dev/null +++ b/okf/docs/decisions/ADR-008-vault-lifecycle-api.md @@ -0,0 +1,93 @@ +# ADR-008: okf's lifecycle API is `Bundle.init/open/join`; the free-function wrappers go + +## Status +Accepted + +## Date +2026-07-31 (amended 2026-08-01: `Vault` → `Bundle` per maintainer review) + +## Context +The Stylist agent's confusion (experiment log, Stylist #1) was filed as a +documentation gap in ADR-007: the store README's headline snippet uses +`initVault('./vault')` where reopen is meant. Maintainer review reframed +it twice: + +1. The snippet is a symptom — the API bakes a noun into every verb + (`initVault`, `openVault`, `joinVault`) that disambiguates nothing. + Investigation confirmed: both systems already carry identical statics + (`init`/`open`/`join` on the store class, okf `store/lib/vault.js:151–165`, + vault-ld `:125–139`); okf's free functions are one-line wrappers over + them; no competing `open{Foo}`/`init{Foo}` exists (only an internal + `openInner`); the SPEC never names the wrappers; the packages are + unpublished. +2. The noun itself is imported vocabulary. vault-ld was written first; + "vault" leaked into its sibling. Upstream OKF (v0.2, the pointer in + `okf/OKF.SPEC.v2.md`) defines the **bundle** as "a self-contained, + hierarchical collection of knowledge documents. The unit of + distribution" — and defines no vault concept at all. auto-okf's own + README opens with "A multi-writer knowledge **bundle** on autobee." + The thing this class opens *is* an OKF bundle, in its authoritative + multi-writer form. + +Known collision, resolved by framing: the current GLOSSARY pins +`bundle` = the on-disk materialized face, `vault` = the store. Under +OKF's ontology these are two forms of **one** bundle — the authoritative +(log) form and the emitted (tree) form. This ADR renames only the API +class; the wholesale vocabulary migration (GLOSSARY, `okf-vault.json` +marker, CLI strings and `` argument names, SPEC prose) is +ADR-009's scope. + +## Decision +In okf, remove `initVault`, `openVault`, `joinVault` from the store's +exports and rename the exported class `Vault` → `Bundle` +(`store/lib/vault.js` → `store/lib/bundle.js`). The class is the sole +lifecycle entry point: + +```js +import { Bundle } from '@auto-okf/store' +const bundle = await Bundle.init('./storage') // create +const bundle = await Bundle.open('./storage') // reopen +const bundle = await Bundle.join('./storage', key) +``` + +vault-ld is unchanged: it exports `Vault` with the same static shape, +and a *vault* is the correct noun for its own ontology (a wiki's +multi-writer store). The twins keep symmetry of **shape** +(`.init/open/join`) while each speaks its own domain's noun. + +Update all in-repo okf callers and docs (22 files: tests, test helpers, +harness writer, CLI `lib/run.js`, three examples, faces internals, three +READMEs — whose headline snippets then teach create-vs-open by +construction). + +## Alternatives Considered + +### Keep wrappers, fix the README snippet (ADR-007 item 2) +Superseded. Two names for one operation caused the confusion; prose +can't fix a redundant vocabulary. + +### `Vault.init/open/join` in okf (this ADR's original form) +Rejected on maintainer review. Cross-system symmetry argued for one +class name, but the symmetry that matters is the shape of the statics; +the noun should match each system's ontology, and OKF's unit is the +bundle — "vault" appears nowhere in the upstream spec. + +### Bare free functions `init`/`open`/`join` +Rejected: unreadable as bare imports at call sites, and diverges from +the class idiom both systems already share. + +## Consequences +- okf's API says what OKF means: you open a bundle. +- Residual "vault" vocabulary inside okf (marker filename, CLI + messages/arg names, GLOSSARY, SPEC prose) is temporarily inconsistent + with the class name — accepted, tracked as ADR-009 so the migration + happens once, deliberately, alongside the v0.2 conformance audit. +- Purely in-repo mechanical migration (unpublished; no shim). + +## Acceptance +- [ ] `grep -r 'initVault\|openVault\|joinVault' okf/` (excluding + node_modules and docs/decisions) returns nothing. +- [ ] okf/store exports `Bundle`; no `Vault` export remains in okf. +- [ ] Full okf test suite green after the sweep. +- [ ] All three okf README headline snippets use `Bundle.init` / + `Bundle.open` and compile as written against the exported surface. diff --git a/okf/docs/decisions/ADR-009-okf-v02-alignment.md b/okf/docs/decisions/ADR-009-okf-v02-alignment.md new file mode 100644 index 0000000..94a836f --- /dev/null +++ b/okf/docs/decisions/ADR-009-okf-v02-alignment.md @@ -0,0 +1,77 @@ +# ADR-009: Align okf with upstream OKF v0.2 — vocabulary migration + conformance audit + +## Status +Accepted + +## Date +2026-08-01 + +## Context +`okf/OKF.SPEC.v2.md` pins upstream: the head OKF spec at +`GoogleCloudPlatform/knowledge-catalog` now declares **v0.2**. Checked +2026-08-01: + +- Bundle remains the unit: "A self-contained, hierarchical collection of + knowledge documents. The unit of distribution." No vault concept + exists in OKF. +- **Breaking changes from v0.1** that touch what auto-okf emits and + ingests: `timestamp` replaced by `generated: { by, at }`; the body + `# Citations` list superseded by a frontmatter `sources` field. +- **New in v0.2**: the actor convention for identity fields; lifecycle + fields `status`, `stale_after`; credibility signals (`author`, + `usage_count`, `last_modified`); the "Attested Computation" concept + type. + +Separately, ADR-008 renamed okf's API class to `Bundle` but deliberately +left the rest of okf's "vault" vocabulary in place, creating a tracked +inconsistency. + +## Decision +One deliberate alignment pass, two parts: + +1. **Vocabulary migration (vault → bundle) across okf surfaces**, with an + inventory decided item-by-item: GLOSSARY entries (the store and the + tree become two forms of one bundle — authoritative form vs emitted + form); the `okf-vault.json` marker filename (coordinate with ADR-006, + whose guardrail reads it in both CLIs); CLI messages ("not a vault") + and argument names (``); SPEC.md prose; harness/example + identifiers. vault-ld keeps its vault vocabulary throughout — the + migration is okf-only. +2. **v0.2 conformance audit of the emitted/ingested bundle format**: + frontmatter emission (`timestamp` → `generated: { by, at }`), + citations → `sources`, whether/what auto-okf adopts of the actor + convention and the new lifecycle/credibility fields, and what the + conformance section (§ numbering per v0.2) now requires of + `index.md`/`log.md`. Each divergence lands in DEVIATIONS.md or gets + fixed — no silent drift. + +## Alternatives Considered + +### Fold into ADR-008's sweep +Rejected by the maintainer: the API rename is mechanical and safe today; +the vocabulary/format migration touches on-disk contract (marker +filename, frontmatter fields) and deserves its own review — "would be a +separate issue." + +### Track upstream head continuously +Rejected for now: pin what was audited (v0.2 as of 2026-08-01) and +re-audit on upstream release, rather than chasing main. + +## Consequences +- Closes ADR-008's accepted inconsistency window. +- The marker-filename change interacts with ADR-006's guardrail and with + every existing vault on disk (the experiment vault included) — + migration/back-compat for the marker must be part of this ADR's + implementation, not an afterthought. +- The `generated`/`sources` changes may ripple to consumers of emitted + bundles (e.g. downstream corpus tooling that read `timestamp`). + +## Acceptance +- [ ] Written inventory of every okf "vault" surface with a + migrate/keep decision per item. +- [ ] Emitted bundles conform to v0.2 (or each divergence is recorded in + DEVIATIONS.md with rationale). +- [ ] Existing vaults created before the marker change still open + (back-compat path tested). +- [ ] ADR-006's guardrail updated in the same change as any marker + rename. diff --git a/okf/examples/agent-session/session.js b/okf/examples/agent-session/session.js index c7da5c7..1da184f 100644 --- a/okf/examples/agent-session/session.js +++ b/okf/examples/agent-session/session.js @@ -45,7 +45,7 @@ import { join, dirname, resolve, relative } from 'node:path' import { fileURLToPath } from 'node:url' import crypto from 'hypercore-crypto' import b4a from 'b4a' -import { initVault, Vault } from '@auto-okf/store' +import { Bundle } from '@auto-okf/store' import { materialize, wanted } from '@auto-okf/faces' import { keyspace as K, revHash } from '@auto-okf/core' @@ -127,10 +127,10 @@ async function main() { console.log('=== agent-session: four agents, one shared vault, partitioned then healed ===\n') // ---- session setup: one vault, four writers ----------------------------- - const alpha = await initVault(join(STORAGE, 'alpha'), { keyPair: crypto.keyPair(seed('alpha')) }) - const beta = await Vault.join(join(STORAGE, 'beta'), alpha.key, { keyPair: crypto.keyPair(seed('beta')) }) - const gamma = await Vault.join(join(STORAGE, 'gamma'), alpha.key, { keyPair: crypto.keyPair(seed('gamma')) }) - const delta = await Vault.join(join(STORAGE, 'delta'), alpha.key, { keyPair: crypto.keyPair(seed('delta')) }) + const alpha = await Bundle.init(join(STORAGE, 'alpha'), { keyPair: crypto.keyPair(seed('alpha')) }) + const beta = await Bundle.join(join(STORAGE, 'beta'), alpha.key, { keyPair: crypto.keyPair(seed('beta')) }) + const gamma = await Bundle.join(join(STORAGE, 'gamma'), alpha.key, { keyPair: crypto.keyPair(seed('gamma')) }) + const delta = await Bundle.join(join(STORAGE, 'delta'), alpha.key, { keyPair: crypto.keyPair(seed('delta')) }) const agents = [alpha, beta, gamma, delta] say('alpha', `opened the vault (key ${alpha.keyHex.slice(0, 16)}…); alpha is the genesis indexer`) diff --git a/okf/examples/slop-archive/archive.js b/okf/examples/slop-archive/archive.js index 0b186d0..2a88486 100644 --- a/okf/examples/slop-archive/archive.js +++ b/okf/examples/slop-archive/archive.js @@ -27,7 +27,7 @@ import { mkdir } from 'node:fs/promises' import { join, dirname, resolve, relative } from 'node:path' import { fileURLToPath } from 'node:url' import crypto from 'hypercore-crypto' -import { initVault } from '@auto-okf/store' +import { Bundle } from '@auto-okf/store' import { materialize } from '@auto-okf/faces' import { keyspace as K, OP_TYPES } from '@auto-okf/core' @@ -75,7 +75,7 @@ async function main() { await mkdir(STORAGE, { recursive: true }) console.log('=== slop-archive: an append-only archive an agent cannot rm ===\n') - const vault = await initVault(join(STORAGE, 'vault'), { keyPair: crypto.keyPair(Buffer.alloc(32, 42)) }) + const vault = await Bundle.init(join(STORAGE, 'vault'), { keyPair: crypto.keyPair(Buffer.alloc(32, 42)) }) say(`opened the archive vault (key ${vault.keyHex.slice(0, 16)}…)`) say(`vault-metadata constant: appendOnly = ${vault.metadata.appendOnly} (§8, loaded from disk, immutable)\n`) diff --git a/okf/examples/vlurp-library/import.js b/okf/examples/vlurp-library/import.js index 2a7bedb..744ed66 100644 --- a/okf/examples/vlurp-library/import.js +++ b/okf/examples/vlurp-library/import.js @@ -33,7 +33,7 @@ import { mkdir, readFile, writeFile, cp, rm } from 'node:fs/promises' import { join, dirname, resolve, relative } from 'node:path' import { fileURLToPath } from 'node:url' import crypto from 'hypercore-crypto' -import { initVault } from '@auto-okf/store' +import { Bundle } from '@auto-okf/store' import { materialize } from '@auto-okf/faces' import { keyspace as K, normalizePath } from '@auto-okf/core' @@ -170,7 +170,7 @@ async function main() { say(`staged ${staged.size} files under ${relative(process.cwd(), STAGING)}/ (transient scratch, never committed)\n`) // ---- 2. import: stamp _id + provenance ---------------------------------- - const vault = await initVault(join(STORAGE, 'vault'), { keyPair: crypto.keyPair(Buffer.alloc(32, 77)) }) + const vault = await Bundle.init(join(STORAGE, 'vault'), { keyPair: crypto.keyPair(Buffer.alloc(32, 77)) }) say('--- first import ---') const first = await importStaged(vault, spec.files, staged, spec) diff --git a/okf/faces/README.md b/okf/faces/README.md index f0bee7d..42ed04c 100644 --- a/okf/faces/README.md +++ b/okf/faces/README.md @@ -12,30 +12,30 @@ ops (SPEC §2, §9). ## Usage -Open a vault, write a couple of concepts, materialize the bundle, edit a +Create a vault, write a couple of concepts, materialize the bundle, edit a file by hand, ingest the edit, and inspect the demand index: ```js -import { initVault } from '@auto-okf/store' +import { Bundle } from '@auto-okf/store' import { materialize, createIngestWatcher, wanted } from '@auto-okf/faces' import { readFile, writeFile } from 'node:fs/promises' -const vault = await initVault('./vault') +const bundle = await Bundle.init('./storage') // mint two concepts (the returned tag is each one's _id, SPEC §6) -const { tag: sales } = await vault.createConcept('datasets/sales', 'BigQuery Dataset') -await vault.setField(sales, 'title', 'Sales') -await vault.setField(sales, 'description', 'All sales tables.') -await vault.setBody(sales, null, 'Contains [orders](/tables/orders.md).') +const { tag: sales } = await bundle.createConcept('datasets/sales', 'BigQuery Dataset') +await bundle.setField(sales, 'title', 'Sales') +await bundle.setField(sales, 'description', 'All sales tables.') +await bundle.setBody(sales, null, 'Contains [orders](/tables/orders.md).') -const { tag: orders } = await vault.createConcept('tables/orders', 'BigQuery Table') -await vault.setField(orders, 'title', 'Orders') -await vault.addTag(orders, 'sales') -await vault.setBody(orders, null, 'One row per order.') -await vault.update() +const { tag: orders } = await bundle.createConcept('tables/orders', 'BigQuery Table') +await bundle.setField(orders, 'title', 'Orders') +await bundle.addTag(orders, 'sales') +await bundle.setBody(orders, null, 'One row per order.') +await bundle.update() // 1. materialize — one .md per live concept, plus derived index.md/log.md -const report = await materialize(vault, './bundle') +const report = await materialize(bundle, './bundle') // ./bundle/index.md (root: okf_version + # Contents listing) // ./bundle/log.md (# Update Log, newest-first date headings) // ./bundle/datasets/sales.md ./bundle/datasets/index.md @@ -47,15 +47,15 @@ const f = './bundle/datasets/sales.md' await writeFile(f, (await readFile(f, 'utf8')).replace('All sales tables.', 'Every sales table.')) // 3. ingest — the edit re-enters as exactly ONE op (a per-field diff) -const watcher = await createIngestWatcher(vault, './bundle', { watch: false }) +const watcher = await createIngestWatcher(bundle, './bundle', { watch: false }) const ops = await watcher.ingestFile('datasets/sales.md') console.log(ops) // [{ type: 'set-field', payload: { id: sales, key: 'description', ... } }] // 4. wanted/ — the ranked demand index (sales links an unwritten concept) -console.log(await wanted(vault)) // [{ path: 'tables/orders', ... }] resolves; unresolved links rank here +console.log(await wanted(bundle)) // [{ path: 'tables/orders', ... }] resolves; unresolved links rank here await watcher.close() -await vault.close() +await bundle.close() ``` Left running (`createIngestWatcher(vault, dir)` without `{ watch: false }`) @@ -98,6 +98,15 @@ that the manifest never tracked (human-authored, not yet ingested) — is `materialize(vault, dir, { forceMirror: true })` restores unconditional mirroring for operator use. +### Verifying content is in the log + +A vault has no privileged bundle directory; the emission manifest +(`.okf-manifest.json`) lives in each target directory, so materializing to +a scratch directory never disturbs another bundle. That makes verification +trivial: materialize to an empty scratch directory — an empty dir that +fills on materialize is proof the content is in the log, because +everything that appears came from it. + ### Ingest: minimal ops, not whole files The watcher diffs frontmatter **per key** against view state and the body @@ -136,13 +145,13 @@ re-derive in the new generation (a head that was an *edit* re-roots at `base:nul so its revHash changes even though the body text is identical — SPEC §14). ```js -import { initVault } from '@auto-okf/store' +import { Bundle } from '@auto-okf/store' import { compact } from '@auto-okf/faces' const res = await compact(old, { toDir: './generation-face', // where the hydrated OKF bundle is written - vaultDir: './vault-e1', // storage dir for the fresh vault - initVault // the @auto-okf/store factory (substrate is injected) + vaultDir: './vault-e1', // storage dir for the fresh vault + initBundle: Bundle.init // the @auto-okf/store factory (substrate is injected) }) // res.vault — the fresh, OPEN vault (the caller closes it) // res.generation — the fresh per-generation corestore namespace id @@ -176,12 +185,12 @@ npm install @auto-okf/faces | --- | --- | | `materialize(vault, dir, opts)` | emit the OKF bundle; `opts.forceMirror` mirrors unconditionally; returns `{ ok, written, unchanged, pendingIngest, removed, stale, unsafe, root }` | | `createIngestWatcher(vault, dir, opts)` | start the watcher; `opts`: `confirmDelete`, `debounce`, `settle`, `watch:false` (drive `ingestFile`/`scan` manually) | -| `compact(vault, opts)` | SPEC §10 generation cycle; `opts`: `initVault` (required store factory), `toDir`, `vaultDir`, `force`, `keyPair`, `encryptionKey`, `sign`, `stageDir`; returns `{ vault, generation, predecessorKey, predecessorPath, predecessorCheckpoint, conflictCopies, logPath, ingestOps }` | +| `compact(vault, opts)` | SPEC §10 generation cycle; `opts`: `initBundle` (required store factory, `Bundle.init`), `toDir`, `vaultDir`, `force`, `keyPair`, `encryptionKey`, `sign`, `stageDir`; returns `{ vault, generation, predecessorKey, predecessorPath, predecessorCheckpoint, conflictCopies, logPath, ingestOps }` | | `wanted(vault)` | `[{ path, count, referrers }]`, most-wanted first (from `wanted/` + `wantedrank/`) | | `readView(reader)` / `readLog(reader)` | the pure view→model projection materialize builds on | | `renderNote` / `parseNote` / `emitFrontmatter` | the canonical YAML note codec | -`vault` is a [`@auto-okf/store`](../store) Vault (or any object exposing +`vault` is a [`@auto-okf/store`](../store) Bundle (or any object exposing `val`, `scan`, `update` and the content-op helpers). Nothing here imports autobee; the substrate is injected. diff --git a/okf/faces/index.js b/okf/faces/index.js index 529da5b..238b35e 100644 --- a/okf/faces/index.js +++ b/okf/faces/index.js @@ -10,7 +10,7 @@ // // The bundle format is normative to the OKF v0.1 SPEC; the canonical YAML // emitter is hand-rolled for byte-determinism (I9). Nothing here imports -// autobee — a @auto-okf/store Vault (or any { val, scan, update } + content +// autobee — a @auto-okf/store Bundle (or any { val, scan, update } + content // helpers) is passed in. export { materialize, renderIndex, renderLog } from './lib/materialize.js' export { createIngestWatcher, IngestWatcher } from './lib/ingest.js' diff --git a/okf/faces/lib/compact.js b/okf/faces/lib/compact.js index 8b23c99..cfec1d3 100644 --- a/okf/faces/lib/compact.js +++ b/okf/faces/lib/compact.js @@ -42,8 +42,8 @@ // the body survives and the new `_rev` re-derives (§14). // // This module imports NO substrate (consistent with the rest of @auto-okf/ -// faces: "the substrate is injected"). The @auto-okf/store `initVault` is -// passed in as `opts.initVault`; the CLI (okf/cli) wires the two together. +// faces: "the substrate is injected"). The @auto-okf/store `Bundle.init` is +// passed in as `opts.initBundle`; the CLI (okf/cli) wires the two together. import { mkdir, readFile, writeFile, rm } from 'node:fs/promises' import { join, dirname } from 'node:path' import { keyspace as K } from '@auto-okf/core' @@ -57,9 +57,9 @@ const GENERATION_LOG_REL = 'log/generation.json' /** * Run one generation cycle. `vault` is the (open) predecessor @auto-okf/store - * Vault. Options: + * Bundle. Options: * - * initVault (REQUIRED) the @auto-okf/store `initVault(dir, opts)` — the + * initBundle (REQUIRED) the @auto-okf/store `Bundle.init(dir, opts)` — the * fresh-vault factory. Injected so faces stays substrate-free. * toDir (REQUIRED) directory to materialize the hydrated face into * (the operator's archival OKF bundle — keeps _id/_rev). @@ -86,9 +86,9 @@ const GENERATION_LOG_REL = 'log/generation.json' */ export async function compact(vault, opts = {}) { const { toDir, vaultDir, force = false, keyPair, encryptionKey, sign } = opts - const initVault = opts.initVault - if (typeof initVault !== 'function') { - throw compactError('COMPACT_NO_INITVAULT', 'opts.initVault (@auto-okf/store initVault) is required') + const initBundle = opts.initBundle + if (typeof initBundle !== 'function') { + throw compactError('COMPACT_NO_INITBUNDLE', 'opts.initBundle (@auto-okf/store Bundle.init) is required') } if (!toDir) throw compactError('COMPACT_NO_TODIR', 'opts.toDir is required') if (!vaultDir) throw compactError('COMPACT_NO_VAULTDIR', 'opts.vaultDir is required') @@ -158,7 +158,7 @@ export async function compact(vault, opts = {}) { if (sig && sig.signature !== undefined) predecessorCheckpoint.signature = sig.signature } - const newVault = await initVault(vaultDir, { + const newVault = await initBundle(vaultDir, { metadata: { predecessorKey, predecessorCheckpoint }, ...(keyPair !== undefined ? { keyPair } : {}), ...(encryptionKey !== undefined ? { encryptionKey } : {}) diff --git a/okf/faces/lib/view.js b/okf/faces/lib/view.js index d29dfde..b44c8da 100644 --- a/okf/faces/lib/view.js +++ b/okf/faces/lib/view.js @@ -1,7 +1,7 @@ // Read the auto-okf view (okf SPEC §4 keyspace) into a structured model the // faces project onto disk. Pure projection of a converged view: the same // view yields the same model on every peer (I8/I9). Reads go through the -// store Vault surface (`val(key)`, `scan(prefix)` — @auto-okf/store), which +// store Bundle surface (`val(key)`, `scan(prefix)` — @auto-okf/store), which // JSON-decodes values; a MemView is adapted by memViewReader() below for // off-substrate tests. import { keyspace as K } from '@auto-okf/core' diff --git a/okf/faces/lib/wanted.js b/okf/faces/lib/wanted.js index d995574..9a1f8c7 100644 --- a/okf/faces/lib/wanted.js +++ b/okf/faces/lib/wanted.js @@ -12,7 +12,7 @@ import { keyspace as K } from '@auto-okf/core' * [{ path, count, referrers: [fromId, ...] }, ...] * ordered most-referenced first (ties broken by path byte order, which the * wantedrank/ key order provides deterministically). `reader` is a store - * Vault (or any { scan } surface); values are JSON-decoded. + * Bundle (or any { scan } surface); values are JSON-decoded. */ export async function wanted(reader) { const referrersByPath = new Map() diff --git a/okf/harness/README.md b/okf/harness/README.md index 45f3c1e..93ccf56 100644 --- a/okf/harness/README.md +++ b/okf/harness/README.md @@ -68,9 +68,9 @@ quality (the partition schedule is the scenario's, not the link's). ## Architecture -- **`lib/writer.js`** — the writer process. Opens an `@auto-okf/store` Vault, +- **`lib/writer.js`** — the writer process. Opens an `@auto-okf/store` Bundle, dials the fault hub for each replication link, and answers controller - commands over fork IPC: `init`/`join`/`open`, `call` (any Vault method — + commands over fork IPC: `init`/`join`/`open`, `call` (any Bundle method — `createConcept`/`setField`/`addTag`/`remTag`/`setBody`/`setPath`/ `deleteConcept`/`resolveConflict`/`addWriter`/`removeWriter`), `append`/ `appendRaw` (raw bytes for poison-op tests), `hash` (snapshot view hash), diff --git a/okf/harness/lib/writer.js b/okf/harness/lib/writer.js index c9fab8d..4ff13c6 100644 --- a/okf/harness/lib/writer.js +++ b/okf/harness/lib/writer.js @@ -5,14 +5,13 @@ // hub over local TCP (see hub.js). // // This is the vault-ld harness writer retargeted to speak the auto-okf op -// vocabulary over a REAL @auto-okf/store Vault (autobee + corestore on +// vocabulary over a REAL @auto-okf/store Bundle (autobee + corestore on // disk). The command surface is unchanged (init/join/open, call/append/ // appendRaw, hash, dump, link/unlink for pause/resume replication, crash = // SIGKILL, restart = respawn) — only the imported store/faces and the op // helpers differ. import net from 'node:net' -import { Vault } from '../../store/index.js' -import { initVault } from '../../store/index.js' +import { Bundle } from '../../store/index.js' import { materialize, createIngestWatcher, wanted, compact } from '../../faces/index.js' let vault = null @@ -51,17 +50,17 @@ function attach(v) { const handlers = { async init({ dir, metadata }) { - attach(await Vault.init(dir, metadata ? { metadata } : {})) + attach(await Bundle.init(dir, metadata ? { metadata } : {})) return info() }, async join({ dir, key, metadata }) { - attach(await Vault.join(dir, key, metadata ? { metadata } : {})) + attach(await Bundle.join(dir, key, metadata ? { metadata } : {})) return info() }, async open({ dir }) { - attach(await Vault.open(dir)) + attach(await Bundle.open(dir)) return info() }, @@ -199,7 +198,7 @@ const handlers = { stream.destroy() } links.clear() - const res = await compact(vault, { toDir, vaultDir, initVault, force }) + const res = await compact(vault, { toDir, vaultDir, initBundle: Bundle.init, force }) const old = vault attach(res.vault) await old.close() diff --git a/okf/store/README.md b/okf/store/README.md index 42d4d69..0ac4678 100644 --- a/okf/store/README.md +++ b/okf/store/README.md @@ -12,16 +12,16 @@ autobee is a standalone multiwriter Hyperbee on hyperbee2 + hypercore 11. Init a vault, append an op, converge a second writer: ```js -import { initVault, joinVault } from '@auto-okf/store' +import { Bundle } from '@auto-okf/store' -const a = await initVault('./vault-a') +const a = await Bundle.init('./storage-a') // mint a concept — the returned tag is its _id (SPEC §6) const { tag: id } = await a.createConcept('guides/getting-started', 'guide') await a.setBody(id, null, 'Read the [architecture](/guides/architecture) first.') // a second peer joins by key and replicates directly -const b = await joinVault('./vault-b', a.key) +const b = await Bundle.join('./storage-b', a.key) const s1 = a.replicate(true) const s2 = b.replicate(false) s1.pipe(s2).pipe(s1) @@ -69,9 +69,9 @@ byte-identical on every peer — `metadataHash` is the cross-peer pin): ## API ```js -const vault = await initVault(dir, opts) // create (writes metadata) -const vault = await openVault(dir, opts) // reopen from disk -const vault = await joinVault(dir, key, opts) // join by vault key +const bundle = await Bundle.init(dir, opts) // create (writes metadata) +const bundle = await Bundle.open(dir, opts) // reopen from disk +const bundle = await Bundle.join(dir, key, opts) // join by vault key ``` `opts`: `metadata` (invite overrides incl. generation-chain fields), @@ -79,7 +79,7 @@ const vault = await joinVault(dir, key, opts) // join by vault key encrypted at rest; persisted so reopen needs no key), `keyPair`, `swarm`, `backpressure`. -### Vault +### Bundle | member | what | | --- | --- | diff --git a/okf/store/index.js b/okf/store/index.js index 60eff96..466607e 100644 --- a/okf/store/index.js +++ b/okf/store/index.js @@ -4,7 +4,7 @@ // §10 generation chain), §8 governance plumbing, hyperswarm replication, and // opt-in append backpressure. @auto-okf/core supplies the deterministic // apply; this package supplies the transport it folds over. -export { Vault } from './lib/vault.js' +export { Bundle } from './lib/bundle.js' export { ViewAdapter } from './lib/view.js' export { viewHash } from './lib/hash.js' export { @@ -22,25 +22,3 @@ export { loadMetadata, keyHex } from './lib/metadata.js' - -import { Vault } from './lib/vault.js' - -/** - * SPEC §13: create a brand-new vault at `dir` — writes the pinned vault - * metadata (okf-vault.json), including the immutable `appendOnly: true` - * constant (§8) and the generation-chain fields `{ predecessorKey, - * predecessorCheckpoint }` (§10, via `opts.metadata`). - */ -export function initVault(dir, opts = {}) { - return Vault.init(dir, opts) -} - -/** SPEC §13: open a vault previously initialized/joined at `dir`. */ -export function openVault(dir, opts = {}) { - return Vault.open(dir, opts) -} - -/** Join an existing vault by key (plus invite metadata / encryptionKey). */ -export function joinVault(dir, key, opts = {}) { - return Vault.join(dir, key, opts) -} diff --git a/okf/store/lib/vault.js b/okf/store/lib/bundle.js similarity index 98% rename from okf/store/lib/vault.js rename to okf/store/lib/bundle.js index 231ea85..ce5cb3c 100644 --- a/okf/store/lib/vault.js +++ b/okf/store/lib/bundle.js @@ -1,4 +1,4 @@ -// Vault (okf SPEC §13 store deliverable): multiwriter vault lifecycle on +// Bundle (okf SPEC §13 store deliverable): multiwriter vault lifecycle on // autobee — corestore in (writer cores derived under the §10 per-generation // namespace), Autobee up, @auto-okf/core's deterministic apply wired // through the ViewAdapter into autobee's apply — plus §8 governance op @@ -29,7 +29,7 @@ // update()/updated()/flush(), replicate(isInitiator|stream). // - db.close() closes the corestore it was handed (index.js _close): we // hand it the per-generation namespace SESSION; the root corestore is closed -// by Vault.close() (corestore@7.11.0 index.js: closing a session never +// by Bundle.close() (corestore@7.11.0 index.js: closing a session never // closes the root; closing the root closes every session). // - genesis: autobee auto-adds the first writer with full permissions // (index.js _processApplyBatch: `if (this.system.isGenesis()) @@ -92,7 +92,7 @@ const GENERATION_NS = 'okf/generation/' const DEFAULT_MAX_INFLIGHT = 1 const DEFAULT_ACK_TIMEOUT = 1500 -export class Vault extends EventEmitter { +export class Bundle extends EventEmitter { /** * @param {string|Corestore} storage directory (preferred: metadata is * pinned to `/okf-vault.json`) or a dedicated Corestore the vault @@ -149,21 +149,21 @@ export class Vault extends EventEmitter { /** Create a brand-new vault (writes pinned metadata at init). */ static async init(storage, opts = {}) { - const vault = new Vault(storage, opts) + const vault = new Bundle(storage, opts) await vault.ready() return vault } /** Open a vault previously initialized/joined at `storage`. */ static async open(storage, opts = {}) { - const vault = new Vault(storage, opts) + const vault = new Bundle(storage, opts) await vault.ready() return vault } /** Join an existing vault by key (plus invite metadata/encryptionKey). */ static async join(storage, key, opts = {}) { - const vault = new Vault(storage, { ...opts, key }) + const vault = new Bundle(storage, { ...opts, key }) await vault.ready() return vault } @@ -386,7 +386,7 @@ export class Vault extends EventEmitter { this._ownSwarm = false } // db.close() closes the generation SESSION (autobee _close); the root - // corestore — which the Vault owns either way — closes after it. + // corestore — which the Bundle owns either way — closes after it. if (this.db) await this.db.close() await this.store.close() } diff --git a/okf/store/lib/view.js b/okf/store/lib/view.js index e32ef68..46fb4f5 100644 --- a/okf/store/lib/view.js +++ b/okf/store/lib/view.js @@ -58,7 +58,7 @@ export class ViewAdapter { /** * `{ seq, key: string, value: string } | null` — value is the raw stored - * JSON string (core's Tx.val JSON-parses it; Vault.val does too). + * JSON string (core's Tx.val JSON-parses it; Bundle.val does too). */ async get(key) { const node = await this.bee.get(toBuf(key)) diff --git a/okf/test/cli.test.js b/okf/test/cli.test.js index dac333f..c9e21d4 100644 --- a/okf/test/cli.test.js +++ b/okf/test/cli.test.js @@ -10,7 +10,7 @@ import { execFile, spawn } from 'node:child_process' import { mkdir, readFile, writeFile, access } from 'node:fs/promises' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' -import { initVault } from '../store/index.js' +import { Bundle } from '../store/index.js' import { keyspace as K } from '../core/index.js' const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..') @@ -182,7 +182,7 @@ test('join + add-writer + remove-writer round trip (§8 governance plumbing)', a * ResolveConflict. Returns { id, keep, supersede }. */ async function seedConflict(storage) { - const v = await initVault(storage) + const v = await Bundle.init(storage) const { tag: id } = await v.createConcept('notes/a', 'note') await v.setBody(id, null, 'version one') await v.update() @@ -234,7 +234,7 @@ test('flags lists a seeded conflict; compact refuses until resolved; resolve cle test('undelete restores a tombstoned concept', async () => { const base = await tmpDir('undelete') const storage = join(base, 'vault') - const v = await initVault(storage) + const v = await Bundle.init(storage) const { tag: id } = await v.createConcept('notes/gone', 'note') await v.setBody(id, null, 'still here') await v.deleteConcept(id) diff --git a/okf/test/faces-helpers.js b/okf/test/faces-helpers.js index 41d72f2..9114435 100644 --- a/okf/test/faces-helpers.js +++ b/okf/test/faces-helpers.js @@ -7,7 +7,7 @@ import { mkdir, readdir, readFile } from 'node:fs/promises' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' -export { initVault, joinVault, openVault } from '../store/index.js' +export { Bundle } from '../store/index.js' export { keyspace as K, canonical, makeTag } from '../core/index.js' export { materialize, createIngestWatcher, wanted, readView } from '../faces/index.js' export { checkOkfConformance } from './okf-conformance.js' diff --git a/okf/test/faces-ingest.test.js b/okf/test/faces-ingest.test.js index f6db160..1697228 100644 --- a/okf/test/faces-ingest.test.js +++ b/okf/test/faces-ingest.test.js @@ -6,8 +6,7 @@ import assert from 'node:assert/strict' import { readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { - initVault, - joinVault, + Bundle, materialize, createIngestWatcher, K, @@ -28,7 +27,7 @@ async function seedOne(v) { } test('ingest of a single field edit emits exactly one op (minimality)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) await seedOne(v) const dir = await tmpDir('bundle') await materialize(v, dir) @@ -48,7 +47,7 @@ test('ingest of a single field edit emits exactly one op (minimality)', async () }) test('ingest of a body edit emits exactly one SetBody citing the file _rev', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const id = await seedOne(v) const dir = await tmpDir('bundle') await materialize(v, dir) @@ -71,7 +70,7 @@ test('ingest of a body edit emits exactly one SetBody citing the file _rev', asy }) test('full roundtrip: materialize -> edit -> ingest -> materialize is stable', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) await seedOne(v) const dir = await tmpDir('bundle') await materialize(v, dir) @@ -100,12 +99,12 @@ test('full roundtrip: materialize -> edit -> ingest -> materialize is stable', a }) test('rename + concurrent edit merge survives the roundtrip', async () => { - const a = await initVault(await tmpDir('vault-a')) + const a = await Bundle.init(await tmpDir('vault-a')) const { tag: id } = await a.createConcept('notes/draft', 'Note') await a.setBody(id, null, 'version zero') await a.update() - const b = await joinVault(await tmpDir('vault-b'), a.key) + const b = await Bundle.join(await tmpDir('vault-b'), a.key) let unpair = pair(a, b) assert.ok(await converged([a, b]), 'initial converge failed') await a.writers.add(b.localKey) @@ -139,7 +138,7 @@ test('rename + concurrent edit merge survives the roundtrip', async () => { }) test('deleting a file emits DeleteConcept only when the guard confirms', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const id = await seedOne(v) const dir = await tmpDir('bundle') await materialize(v, dir) @@ -167,7 +166,7 @@ test('deleting a file emits DeleteConcept only when the guard confirms', async ( }) test('adopting an unknown _id creates the concept (generation-cycle contract)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const dir = await tmpDir('bundle') await materialize(v, dir) // establish the manifest + reserved faces diff --git a/okf/test/faces-materialize.test.js b/okf/test/faces-materialize.test.js index fcbda7a..8450827 100644 --- a/okf/test/faces-materialize.test.js +++ b/okf/test/faces-materialize.test.js @@ -4,8 +4,7 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { - initVault, - joinVault, + Bundle, materialize, wanted, checkOkfConformance, @@ -37,7 +36,7 @@ async function seed(v) { } test('materialize is byte-deterministic across two runs of the same view (I9)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) await seed(v) const dir1 = await tmpDir('bundle') const dir2 = await tmpDir('bundle') @@ -50,9 +49,9 @@ test('materialize is byte-deterministic across two runs of the same view (I9)', }) test('materialize is byte-identical on a replicated peer (I9 convergence)', async () => { - const a = await initVault(await tmpDir('vault-a')) + const a = await Bundle.init(await tmpDir('vault-a')) await seed(a) - const b = await joinVault(await tmpDir('vault-b'), a.key) + const b = await Bundle.join(await tmpDir('vault-b'), a.key) const unpair = pair(a, b) const h = await converged([a, b]) assert.ok(h, 'peers did not converge') @@ -71,7 +70,7 @@ test('materialize is byte-identical on a replicated peer (I9 convergence)', asyn test('materialize is byte-identical on a from-scratch rebuilt view (I3/I9)', async () => { const dir = await tmpDir('vault') - const a = await initVault(dir) + const a = await Bundle.init(dir) const ids = await seed(a) const dir1 = await tmpDir('bundle') await materialize(a, dir1) @@ -79,8 +78,7 @@ test('materialize is byte-identical on a from-scratch rebuilt view (I3/I9)', asy await a.close() // Reopen from disk (a full incremental re-fold from the persisted log). - const { openVault } = await import('../store/index.js') - const b = await openVault(dir) + const b = await Bundle.open(dir) await b.update() const dir2 = await tmpDir('bundle') await materialize(b, dir2) @@ -91,7 +89,7 @@ test('materialize is byte-identical on a from-scratch rebuilt view (I3/I9)', asy }) test('emitted bundle passes the independent OKF v0.1 conformance check', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) await seed(v) const dir = await tmpDir('bundle') await materialize(v, dir) @@ -105,7 +103,7 @@ test('emitted bundle passes the independent OKF v0.1 conformance check', async ( }) test('frontmatter emission order is _id, _rev, type, then sorted keys (§6)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const { orders } = await seed(v) const dir = await tmpDir('bundle') await materialize(v, dir) @@ -126,7 +124,7 @@ test('frontmatter emission order is _id, _rev, type, then sorted keys (§6)', as }) test('root index.md carries okf_version and lists concepts then subdirs (OKF §6)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) await seed(v) const dir = await tmpDir('bundle') await materialize(v, dir) @@ -143,7 +141,7 @@ test('root index.md carries okf_version and lists concepts then subdirs (OKF §6 }) test('log.md is # Update Log with ISO date headings newest-first (OKF §7)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) await seed(v) const dir = await tmpDir('bundle') await materialize(v, dir) @@ -157,7 +155,7 @@ test('log.md is # Update Log with ISO date headings newest-first (OKF §7)', asy }) test('wanted() ranks unwritten concepts by referrer count (§4/§9)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) // two concepts both link to the same unwritten target; one also links a // less-referenced target. const { tag: a } = await v.createConcept('a', 'Note') diff --git a/okf/test/faces-preservation.test.js b/okf/test/faces-preservation.test.js index df46ece..b046912 100644 --- a/okf/test/faces-preservation.test.js +++ b/okf/test/faces-preservation.test.js @@ -8,8 +8,7 @@ import assert from 'node:assert/strict' import { readFile, writeFile, stat, mkdir } from 'node:fs/promises' import { join } from 'node:path' import { - initVault, - joinVault, + Bundle, materialize, createIngestWatcher, K, @@ -30,7 +29,7 @@ const exists = async (p) => { } test('N3: a hand-authored .md is never deleted or overwritten by materialize', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const { tag: id } = await v.createConcept('a', 'Note') await v.setBody(id, null, 'concept a') await v.update() @@ -52,7 +51,7 @@ test('N3: a hand-authored .md is never deleted or overwritten by materialize', a }) test('N3: a failed ingest (type-less file) never lets materialize delete the human file', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const { tag: id } = await v.createConcept('a', 'Note') await v.setBody(id, null, 'a') await v.update() @@ -78,7 +77,7 @@ test('N3: a failed ingest (type-less file) never lets materialize delete the hum }) test('N3: a renamed concept\'s old path is pruned from the manifest (no stale later delete)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const { tag: id } = await v.createConcept('a', 'Note') await v.setBody(id, null, 'body') await v.update() @@ -107,7 +106,7 @@ test('N3: a renamed concept\'s old path is pruned from the manifest (no stale la }) test('N3: a human EDIT to a materialized concept file is preserved, not clobbered', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const { tag: id } = await v.createConcept('a', 'Note') await v.setField(id, 'title', 'A') await v.setBody(id, null, 'body') @@ -127,7 +126,7 @@ test('N3: a human EDIT to a materialized concept file is preserved, not clobbere }) test('materialize removes only its own prior outputs (renamed concept old path)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const { tag: id } = await v.createConcept('old/name', 'Note') await v.setBody(id, null, 'x') await v.update() @@ -147,7 +146,7 @@ test('materialize removes only its own prior outputs (renamed concept old path)' }) test('forceMirror overwrites and deletes unconditionally (operator escape hatch)', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const { tag: id } = await v.createConcept('a', 'Note') await v.setBody(id, null, 'body') await v.update() @@ -168,8 +167,8 @@ test('forceMirror overwrites and deletes unconditionally (operator escape hatch) }) test('conflict-copy round-trip: a pathcollision loser materializes and re-ingests stably', async () => { - const a = await initVault(await tmpDir('vault-a')) - const b = await joinVault(await tmpDir('vault-b'), a.key) + const a = await Bundle.init(await tmpDir('vault-a')) + const b = await Bundle.join(await tmpDir('vault-b'), a.key) let unpair = pair(a, b) assert.ok(await converged([a, b]), 'initial converge failed') await a.writers.add(b.localKey) diff --git a/okf/test/i11-roundtrip.test.js b/okf/test/i11-roundtrip.test.js index fef94eb..e550047 100644 --- a/okf/test/i11-roundtrip.test.js +++ b/okf/test/i11-roundtrip.test.js @@ -12,7 +12,7 @@ import assert from 'node:assert/strict' import { mkdir, readdir, readFile } from 'node:fs/promises' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' -import { initVault, joinVault } from '../store/index.js' +import { Bundle } from '../store/index.js' import { materialize, compact, readView, readLog } from '../faces/index.js' import { keyspace as K } from '../core/index.js' @@ -178,7 +178,7 @@ async function buildNonTrivial(v) { } test('I11: a generation cycle round-trips a non-trivial bundle (state + identity fidelity)', async () => { - const old = await initVault(await tmpDir('old-vault')) + const old = await Bundle.init(await tmpDir('old-vault')) const ids = await buildNonTrivial(old) await old.update() @@ -197,7 +197,7 @@ test('I11: a generation cycle round-trips a non-trivial bundle (state + identity const res = await compact(old, { toDir: await tmpDir('compact-face'), vaultDir: await tmpDir('new-vault'), - initVault + initBundle: Bundle.init }) const nu = res.vault @@ -268,7 +268,7 @@ test('I11: a generation cycle round-trips a non-trivial bundle (state + identity }) test('I11 guardrail: compact refuses an undrained conflict without force, proceeds with it', async () => { - const v = await initVault(await tmpDir('conf-vault')) + const v = await Bundle.init(await tmpDir('conf-vault')) const { tag: id } = await v.createConcept('c', 'Note') await v.setBody(id, null, 'r1') await v.update() @@ -285,7 +285,7 @@ test('I11 guardrail: compact refuses an undrained conflict without force, procee const rejFace = await tmpDir('rej-face') const rejVault = await tmpDir('rej-vault') await assert.rejects( - () => compact(v, { toDir: rejFace, vaultDir: join(rejVault, 'x'), initVault }), + () => compact(v, { toDir: rejFace, vaultDir: join(rejVault, 'x'), initBundle: Bundle.init }), (err) => err.code === 'COMPACT_REFUSED', 'compact must refuse an open conflict without force' ) @@ -295,7 +295,7 @@ test('I11 guardrail: compact refuses an undrained conflict without force, procee const res = await compact(v, { toDir: await tmpDir('force-face'), vaultDir: await tmpDir('force-vault'), - initVault, + initBundle: Bundle.init, force: true }) const nu = res.vault @@ -310,19 +310,19 @@ test('I11 guardrail: compact refuses an undrained conflict without force, procee }) test('I11: a second generation cycle (generation chain of length 2) still round-trips', async () => { - const gen = await initVault(await tmpDir('gen-vault')) + const gen = await Bundle.init(await tmpDir('gen-vault')) await buildNonTrivial(gen) await gen.update() // cycle 1: root -> generation1 const dir1 = await tmpDir('e1-bundle') - const res1 = await compact(gen, { toDir: await tmpDir('e1-face'), vaultDir: await tmpDir('e1-vault'), initVault }) + const res1 = await compact(gen, { toDir: await tmpDir('e1-face'), vaultDir: await tmpDir('e1-vault'), initBundle: Bundle.init }) const e1 = res1.vault const b1 = await readBundle((await materialize(e1, dir1), dir1)) // cycle 2: generation1 -> generation2 const dir2 = await tmpDir('e2-bundle') - const res2 = await compact(e1, { toDir: await tmpDir('e2-face'), vaultDir: await tmpDir('e2-vault'), initVault }) + const res2 = await compact(e1, { toDir: await tmpDir('e2-face'), vaultDir: await tmpDir('e2-vault'), initBundle: Bundle.init }) const e2 = res2.vault const b2 = await readBundle((await materialize(e2, dir2), dir2)) @@ -347,8 +347,8 @@ test('I11: a second generation cycle (generation chain of length 2) still round- }) test('I11: --force carries a path-collision conflict copy across the generation (old path -> new path)', async () => { - const a = await initVault(await tmpDir('cc-a')) - const b = await joinVault(await tmpDir('cc-b'), a.key) + const a = await Bundle.init(await tmpDir('cc-a')) + const b = await Bundle.join(await tmpDir('cc-b'), a.key) let unpair = pair(a, b) assert.ok(await converged([a, b]), 'initial converge failed') await a.writers.add(b.localKey) @@ -367,7 +367,7 @@ test('I11: --force carries a path-collision conflict copy across the generation const ccRejFace = await tmpDir('cc-rej-face') const ccRejVault = await tmpDir('cc-rej-vault') await assert.rejects( - () => compact(a, { toDir: ccRejFace, vaultDir: join(ccRejVault, 'x'), initVault }), + () => compact(a, { toDir: ccRejFace, vaultDir: join(ccRejVault, 'x'), initBundle: Bundle.init }), (err) => err.code === 'COMPACT_REFUSED' ) @@ -375,7 +375,7 @@ test('I11: --force carries a path-collision conflict copy across the generation const res = await compact(a, { toDir: await tmpDir('cc-face'), vaultDir: await tmpDir('cc-vault'), - initVault, + initBundle: Bundle.init, force: true }) const nu = res.vault diff --git a/okf/test/regressions/dirmove-dataloss.test.js b/okf/test/regressions/dirmove-dataloss.test.js index ffd2572..2d75846 100644 --- a/okf/test/regressions/dirmove-dataloss.test.js +++ b/okf/test/regressions/dirmove-dataloss.test.js @@ -21,12 +21,12 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { mkdir, readFile, rename, writeFile } from 'node:fs/promises' import { join } from 'node:path' -import { initVault, createIngestWatcher, materialize, K, until, tmpDir } from '../faces-helpers.js' +import { Bundle, createIngestWatcher, materialize, K, until, tmpDir } from '../faces-helpers.js' const BODY = 'A precious body the human wrote.\n' async function seededBundle(name, path = 'Recipes/Soup') { - const v = await initVault(await tmpDir(name)) + const v = await Bundle.init(await tmpDir(name)) const { tag: id } = await v.createConcept(path, 'Recipe') await v.setField(id, 'title', 'Soup') await v.setBody(id, null, BODY) diff --git a/okf/test/regressions/double-conflict-suffix-ingest.test.js b/okf/test/regressions/double-conflict-suffix-ingest.test.js index bdff10d..d29a497 100644 --- a/okf/test/regressions/double-conflict-suffix-ingest.test.js +++ b/okf/test/regressions/double-conflict-suffix-ingest.test.js @@ -8,10 +8,10 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { writeFile } from 'node:fs/promises' import { join } from 'node:path' -import { initVault, materialize, createIngestWatcher, K, tmpDir } from '../faces-helpers.js' +import { Bundle, materialize, createIngestWatcher, K, tmpDir } from '../faces-helpers.js' test('P7 (#28): a double-suffixed human file ingests to the fully-stripped base path', async () => { - const v = await initVault(await tmpDir('vault')) + const v = await Bundle.init(await tmpDir('vault')) const dir = await tmpDir('bundle') await materialize(v, dir) // establish the manifest + reserved faces diff --git a/okf/test/regressions/squat-mint-ingest.test.js b/okf/test/regressions/squat-mint-ingest.test.js index b51f0ba..14d8add 100644 --- a/okf/test/regressions/squat-mint-ingest.test.js +++ b/okf/test/regressions/squat-mint-ingest.test.js @@ -13,8 +13,7 @@ import assert from 'node:assert/strict' import { writeFile, mkdir } from 'node:fs/promises' import { join } from 'node:path' import { - initVault, - joinVault, + Bundle, createIngestWatcher, K, makeTag, @@ -25,8 +24,8 @@ import { } from '../faces-helpers.js' test('P1: a squatted mint id does not capture an ingested file’s fields', async () => { - const A = await initVault(await tmpDir('A')) - const E = await joinVault(await tmpDir('E'), A.keyHex) + const A = await Bundle.init(await tmpDir('A')) + const E = await Bundle.join(await tmpDir('E'), A.keyHex) const unpair = pair(A, E) await converged([A, E]) diff --git a/okf/test/store-convergence.test.js b/okf/test/store-convergence.test.js index 0e1a045..8927ffd 100644 --- a/okf/test/store-convergence.test.js +++ b/okf/test/store-convergence.test.js @@ -4,11 +4,11 @@ // writer's own peer, ack-anchored fold, cross-peer hash equality). import { test } from 'node:test' import assert from 'node:assert/strict' -import { Vault, K, tmpDir, pair, until, converged } from './store-helpers.js' +import { Bundle, K, tmpDir, pair, until, converged } from './store-helpers.js' test('two writers converge to bit-identical views (hash equality)', async (t) => { - const a = await Vault.init(await tmpDir('conv-a')) - const b = await Vault.join(await tmpDir('conv-b'), a.key) + const a = await Bundle.init(await tmpDir('conv-a')) + const b = await Bundle.join(await tmpDir('conv-b'), a.key) const unpair = pair(a, b) t.after(async () => { unpair() @@ -60,8 +60,8 @@ test('two writers converge to bit-identical views (hash equality)', async (t) => }) test('optimistic onboarding: held locally, folded at the ack position (§8)', async (t) => { - const a = await Vault.init(await tmpDir('opt-a')) - const c = await Vault.join(await tmpDir('opt-c'), a.key) + const a = await Bundle.init(await tmpDir('opt-a')) + const c = await Bundle.join(await tmpDir('opt-c'), a.key) const unpair = pair(a, c) t.after(async () => { unpair() @@ -116,8 +116,8 @@ test('optimistic onboarding: held locally, folded at the ack position (§8)', as }) test('opt-in append backpressure throttles without deadlocking', async (t) => { - const a = await Vault.init(await tmpDir('bp-a')) - const b = await Vault.join(await tmpDir('bp-b'), a.key, { + const a = await Bundle.init(await tmpDir('bp-a')) + const b = await Bundle.join(await tmpDir('bp-b'), a.key, { backpressure: { maxInflight: 2, ackTimeout: 250 } }) const unpair = pair(a, b) diff --git a/okf/test/store-encryption.test.js b/okf/test/store-encryption.test.js index 6e89f57..966a366 100644 --- a/okf/test/store-encryption.test.js +++ b/okf/test/store-encryption.test.js @@ -5,12 +5,12 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { randomBytes } from 'node:crypto' -import { Vault, K, tmpDir, pair, until, converged } from './store-helpers.js' +import { Bundle, K, tmpDir, pair, until, converged } from './store-helpers.js' test('encrypted vault: reopen from disk without the key, invited peer reads', async (t) => { const encryptionKey = randomBytes(32) const dir = await tmpDir('enc-a') - const a = await Vault.init(dir, { encryptionKey }) + const a = await Bundle.init(dir, { encryptionKey }) assert.equal(a.metadata.encrypted, true, 'metadata pins encryption at rest') const { tag: id } = await a.createConcept('secret/plan', 'plan') @@ -19,7 +19,7 @@ test('encrypted vault: reopen from disk without the key, invited peer reads', as await a.close() // reopen WITHOUT passing the key: restored from the local core userData - const a2 = await Vault.open(dir) + const a2 = await Bundle.open(dir) assert.equal(await a2.snapshotHash(), before, 'reopened encrypted view is intact') assert.equal(a2.metadata.encrypted, true) const head = await a2.val(K.cBody(id)) @@ -27,7 +27,7 @@ test('encrypted vault: reopen from disk without the key, invited peer reads', as assert.equal(rev.text, 'meet at dawn') // an invited peer holding the key converges and reads - const b = await Vault.join(await tmpDir('enc-b'), a2.key, { encryptionKey }) + const b = await Bundle.join(await tmpDir('enc-b'), a2.key, { encryptionKey }) const unpair = pair(a2, b) t.after(async () => { unpair() diff --git a/okf/test/store-helpers.js b/okf/test/store-helpers.js index 24c19f0..f0b0fbb 100644 --- a/okf/test/store-helpers.js +++ b/okf/test/store-helpers.js @@ -7,10 +7,7 @@ import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' export { - Vault, - initVault, - openVault, - joinVault, + Bundle, createMetadata, metadataHash, canonicalMetadataBytes, diff --git a/okf/test/store-metadata.test.js b/okf/test/store-metadata.test.js index 984df42..50f0d36 100644 --- a/okf/test/store-metadata.test.js +++ b/okf/test/store-metadata.test.js @@ -9,7 +9,7 @@ import assert from 'node:assert/strict' import { writeFile, readFile } from 'node:fs/promises' import { join } from 'node:path' import { - Vault, + Bundle, createMetadata, metadataHash, canonicalMetadataBytes, @@ -24,10 +24,10 @@ import { test('vault metadata is deterministic and identical across peers', async (t) => { const dirA = await tmpDir('meta-a') - const a = await Vault.init(dirA) + const a = await Bundle.init(dirA) const dirB = await tmpDir('meta-b') // the invite carries the full pinned document - const b = await Vault.join(dirB, a.key, { metadata: a.metadata }) + const b = await Bundle.join(dirB, a.key, { metadata: a.metadata }) const unpair = pair(a, b) t.after(async () => { unpair() @@ -74,21 +74,21 @@ test('appendOnly is immutable: unrepresentable off, refused when tampered', asyn // a tampered on-disk document cannot open const dir = await tmpDir('meta-tamper') - const v = await Vault.init(dir) + const v = await Bundle.init(dir) await v.close() const path = join(dir, METADATA_FILE) const doc = JSON.parse(await readFile(path, 'utf8')) doc.appendOnly = false await writeFile(path, JSON.stringify(doc)) - await assert.rejects(() => Vault.open(dir), /appendOnly/) + await assert.rejects(() => Bundle.open(dir), /appendOnly/) }) test('a mismatched invite is refused (metadata is convergence-relevant)', async (t) => { const dir = await tmpDir('meta-invite') - const v = await Vault.init(dir) + const v = await Bundle.init(dir) await v.close() await assert.rejects( - () => Vault.open(dir, { metadata: { limits: { maxPathBytes: 512 } } }), + () => Bundle.open(dir, { metadata: { limits: { maxPathBytes: 512 } } }), (err) => err.code === 'VAULT_METADATA_MISMATCH' ) }) @@ -120,12 +120,12 @@ test('generation chain: predecessor fields pin a deterministic writer namespace // per-generation namespace: same directory, different generation → different vault // (writer) key — the §10 keypair-freshness guarantee const dir = await tmpDir('meta-generation') - const genesisVault = await Vault.init(dir) + const genesisVault = await Bundle.init(dir) const genesisKey = genesisVault.keyHex await genesisVault.close() const dir2 = await tmpDir('meta-generation') // fresh dir, generation-2 vault - const next = await Vault.init(dir2, { + const next = await Bundle.init(dir2, { metadata: { predecessorKey, predecessorCheckpoint: checkpoint } }) t.after(() => next.close()) @@ -137,7 +137,7 @@ test('generation chain: predecessor fields pin a deterministic writer namespace // reopen from disk restores the same generation and key await next.close() - const reopened = await Vault.open(dir2) + const reopened = await Bundle.open(dir2) t.after(() => reopened.close()) assert.equal(reopened.generation, e1) assert.equal(reopened.metadata.generation, e1) @@ -147,7 +147,7 @@ test('same corestore directory, different generations → fresh writer keypairs // the hazard §10 names: an operator re-inits in a REUSED directory; the // per-generation namespace must still yield a fresh local writer keypair const dir = await tmpDir('meta-reuse') - const v1 = await Vault.init(dir) + const v1 = await Bundle.init(dir) const k1 = v1.keyHex await v1.close() @@ -155,7 +155,7 @@ test('same corestore directory, different generations → fresh writer keypairs // metadata (operator archived the old vault), init with a predecessor const { rm } = await import('node:fs/promises') await rm(join(dir, METADATA_FILE)) - const v2 = await Vault.init(dir, { + const v2 = await Bundle.init(dir, { metadata: { predecessorKey: k1, predecessorCheckpoint: { viewHash: '0'.repeat(64), writerLengths: [[k1, 1]] } diff --git a/okf/test/store-restart.test.js b/okf/test/store-restart.test.js index e41f0a4..c1496db 100644 --- a/okf/test/store-restart.test.js +++ b/okf/test/store-restart.test.js @@ -4,11 +4,11 @@ // the replicated log converges to the same hash (rebuild equivalence). import { test } from 'node:test' import assert from 'node:assert/strict' -import { Vault, K, tmpDir, pair, until, converged } from './store-helpers.js' +import { Bundle, K, tmpDir, pair, until, converged } from './store-helpers.js' test('restart from disk: reopened view equals the pre-close view (I3)', async (t) => { const dir = await tmpDir('restart') - const a = await Vault.init(dir) + const a = await Bundle.init(dir) const { tag: id } = await a.createConcept('guides/home', 'guide') await a.setField(id, 'title', ['Welcome']) await a.addTag(id, 'wiki') @@ -20,7 +20,7 @@ test('restart from disk: reopened view equals the pre-close view (I3)', async (t assert.ok(beforeKeys.length > 0) await a.close() - const a2 = await Vault.open(dir) + const a2 = await Bundle.open(dir) t.after(() => a2.close()) assert.equal(await a2.snapshotHash(), before, 'hash equality across restart') assert.deepEqual(Object.keys(await a2.view.dump('')), beforeKeys, 'same key set') @@ -36,14 +36,14 @@ test('restart from disk: reopened view equals the pre-close view (I3)', async (t test('fresh peer rebuild from the log equals the restarted view (I3)', async (t) => { const dir = await tmpDir('rebuild-a') - const a = await Vault.init(dir) + const a = await Bundle.init(dir) const { tag: id } = await a.createConcept('guides/rebuild', 'guide') await a.setField(id, 'title', ['Rebuild me']) await a.setBody(id, null, 'body text') await a.close() - const a2 = await Vault.open(dir) - const fresh = await Vault.join(await tmpDir('rebuild-b'), a2.key) + const a2 = await Bundle.open(dir) + const fresh = await Bundle.join(await tmpDir('rebuild-b'), a2.key) const unpair = pair(a2, fresh) t.after(async () => { unpair() diff --git a/okf/test/store-swarm.test.js b/okf/test/store-swarm.test.js index 2c912dd..0a55dc5 100644 --- a/okf/test/store-swarm.test.js +++ b/okf/test/store-swarm.test.js @@ -5,7 +5,7 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { createRequire } from 'node:module' -import { Vault, K, tmpDir, until } from './store-helpers.js' +import { Bundle, K, tmpDir, until } from './store-helpers.js' const storeRequire = createRequire(new URL('../store/index.js', import.meta.url)) const Hyperswarm = storeRequire('hyperswarm') @@ -15,8 +15,8 @@ test('two vaults replicate over hyperswarm (discoveryKey join)', async (t) => { const tn = await createTestnet(3) const swarmA = new Hyperswarm({ bootstrap: tn.bootstrap }) const swarmB = new Hyperswarm({ bootstrap: tn.bootstrap }) - const a = await Vault.init(await tmpDir('swarm-a'), { swarm: swarmA }) - const b = await Vault.join(await tmpDir('swarm-b'), a.key, { swarm: swarmB }) + const a = await Bundle.init(await tmpDir('swarm-a'), { swarm: swarmA }) + const b = await Bundle.join(await tmpDir('swarm-b'), a.key, { swarm: swarmB }) t.after(async () => { await a.close() await b.close() diff --git a/okf/test/store-writers.test.js b/okf/test/store-writers.test.js index 771b54c..4d99376 100644 --- a/okf/test/store-writers.test.js +++ b/okf/test/store-writers.test.js @@ -5,11 +5,11 @@ // cross-peer hash equality of the applied/rejected partition (I10). import { test } from 'node:test' import assert from 'node:assert/strict' -import { Vault, K, tmpDir, pair, until, converged } from './store-helpers.js' +import { Bundle, K, tmpDir, pair, until, converged } from './store-helpers.js' test('writer add/remove round trip: causal revocation, uniform on every peer', async (t) => { - const a = await Vault.init(await tmpDir('wr-a')) - const b = await Vault.join(await tmpDir('wr-b'), a.key) + const a = await Bundle.init(await tmpDir('wr-a')) + const b = await Bundle.join(await tmpDir('wr-b'), a.key) let unpair = pair(a, b) t.after(async () => { unpair() @@ -87,7 +87,7 @@ test('writer add/remove round trip: causal revocation, uniform on every peer', a }) test('remove-writer that would leave zero indexers is rejected (§8)', async (t) => { - const a = await Vault.init(await tmpDir('wr-last')) + const a = await Bundle.init(await tmpDir('wr-last')) t.after(() => a.close()) const aHex = a.localKey.toString('hex') @@ -102,8 +102,8 @@ test('remove-writer that would leave zero indexers is rejected (§8)', async (t) }) test('setWriterPolicy round trip: namespace prefixes enforced at apply', async (t) => { - const a = await Vault.init(await tmpDir('wr-pol-a')) - const b = await Vault.join(await tmpDir('wr-pol-b'), a.key) + const a = await Bundle.init(await tmpDir('wr-pol-a')) + const b = await Bundle.join(await tmpDir('wr-pol-b'), a.key) const unpair = pair(a, b) t.after(async () => { unpair() diff --git a/package.json b/package.json index 89f30b0..675cd26 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@auto-okf/core": "workspace:*", "@auto-okf/faces": "workspace:*", "@auto-okf/store": "workspace:*", + "@auto-okf/cli": "workspace:*", "@autovault-ld/cli": "workspace:*", "@autovault-ld/core": "workspace:*", "@autovault-ld/faces": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0de5e14..d16b5c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@auto-okf/cli': + specifier: workspace:* + version: link:okf/cli '@auto-okf/core': specifier: workspace:* version: link:okf/core