diff --git a/.github/workflows/guard.yml b/.github/workflows/guard.yml index 59dc0c5..df33c41 100644 --- a/.github/workflows/guard.yml +++ b/.github/workflows/guard.yml @@ -1,5 +1,6 @@ -# Publish guard — this repo must never contain engine material. -# Fails the build if forbidden patterns appear anywhere in the tree. +# Publish guard — this repo must never contain engine material or +# private partner names. Fails the build if forbidden patterns appear +# anywhere in the tree. name: publish-guard on: [push, pull_request] jobs: @@ -10,12 +11,23 @@ jobs: - name: Forbidden-content scan run: | set -e - PATTERNS='supabase\.co|SERVICE_ROLE|sk-ant-|sk_live|MODEL_PRICING|pricing\.charge|narrate-|compile-meaning|signals\.ts|governance' + PATTERNS='supabase\.co|SERVICE_ROLE|sk-ant-|sk_live|MODEL_PRICING|pricing\.charge|narrate-|compile-meaning|signals\.ts|governance|shipmemory|posemesh|mentra|auki' if grep -rniE "$PATTERNS" --exclude-dir=.git --exclude=guard.yml .; then - echo "::error::Forbidden pattern found — engine material must never enter this repo." + echo "::error::Forbidden pattern found — engine material and partner names must never enter this repo." exit 1 fi echo "Clean." - - name: Validate examples against schema + - name: Validate capture examples against schema run: | npx --yes -p ajv-cli@5 -p ajv-formats ajv validate --spec=draft2020 -c ajv-formats -s schema/capture.schema.json -r schema/location.schema.json -d "examples/*.bevia-capture.json" + - name: Validate export example lines against schema + run: | + set -e + mkdir -p /tmp/export-lines + i=0 + while IFS= read -r line; do + [ -z "$line" ] && continue + i=$((i+1)) + printf '%s' "$line" > "/tmp/export-lines/line-$i.json" + done < examples/robot-session.bevia-export.jsonl + npx --yes -p ajv-cli@5 -p ajv-formats ajv validate --spec=draft2020 -c ajv-formats -s schema/export-record.schema.json -r schema/location.schema.json -d "/tmp/export-lines/*.json" diff --git a/README.md b/README.md index 1d15fa7..3a9f841 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,31 @@ -# Bevia Capture Protocol - -The open specification for feeding evidence into a [Bevia](https://bevia.co) -engine. If you can write a JSON file, you can build a Bevia sensor. - -- **[SPEC.md](SPEC.md)** — the protocol: capture body, drop-file - convention, location object, behavioral guarantees. Versioned, - additive-only. +# Bevia Protocol + +The open specification for the doors of a [Bevia](https://bevia.co) +engine — how evidence goes in, and how understanding comes back out. +If you can read or write a JSON file, you can build against Bevia. + +- **[SPEC.md](SPEC.md)** — the capture protocol (v1, the way in): + capture body, drop-file convention, location object, behavioral + guarantees. Versioned, additive-only. +- **[SPEC-EXPORT.md](SPEC-EXPORT.md)** — the export protocol (v1.1, + the way out): export records, drop-file and pull conventions, + provenance guarantees. - **[schema/](schema/)** — JSON Schemas for validation. -- **[examples/](examples/)** — real capture files: a chat session, a - robot maintenance session, a captured note. +- **[examples/](examples/)** — real capture files (a chat session, a + robot maintenance session, a captured note) and a matching export. - **[reference/](reference/)** — reference sensor implementations. ## The one-paragraph version Bevia turns observed work — conversations, sessions, notes — into a -navigable, queryable memory. This repo specifies the **doors**: the -shape of a capture, how a file in a watched folder becomes evidence -(`*.bevia-capture.json` — the file's content *is* the request body), -and how a sensor stamps *where* knowledge happened. Everything here is -open and free to implement, forever. +navigable, queryable memory. This repo specifies the **doors**. In: +the shape of a capture, how a file in a watched folder becomes +evidence (`*.bevia-capture.json` — the file's content *is* the +request body), and how a sensor stamps *where* knowledge happened. +Out: the export record, how the engine's understanding lands as +files or answers a pull (`*.bevia-export.jsonl`), and the guarantee +that every record traces back to evidence. Everything here is open +and free to implement, forever. **What this repo is not:** the engine. How captures become understanding — the compilation, the cartography, the narration — is @@ -27,12 +34,19 @@ license; running an engine does. Open doors, paid brain. ## Quick start +**In (sensor):** + 1. Write a capture file (copy one from [examples/](examples/)). 2. Name it `anything.bevia-capture.json`. 3. Put it in a folder your Bevia app watches. -That's the whole integration. For a continuous sensor, see the -reference implementations. +**Out (consumer):** + +1. Point your Bevia app's export at a folder. +2. Read `*.bevia-export.jsonl` — one record per line, deduped by `id`. + +That's the whole integration, both directions. For a continuous +sensor, see the reference implementations. ## License diff --git a/SPEC-EXPORT.md b/SPEC-EXPORT.md new file mode 100644 index 0000000..006f565 --- /dev/null +++ b/SPEC-EXPORT.md @@ -0,0 +1,173 @@ +# The Bevia Export Protocol — v1.1 (draft) + +This document specifies how understanding leaves a Bevia engine: the +export record, the drop-file convention, and the pull convention. It +is the complete public contract for building a **consumer** — any +tool that reads what an engine has come to understand. It is the +companion to [SPEC.md](SPEC.md) (the capture protocol, v1): captures +go in through one door, records come out through this one. + +**Scope.** This spec describes the engine's *exit doors*: what comes +out, and what a consumer may rely on. It does not describe the engine +— how captures become understanding is proprietary and out of scope, +and no part of this spec depends on it. + +--- + +## 1. The export record + +An export is a stream of **records** — one JSON object per exported +thing. Every record has the same envelope: + +```json +{ + "record": { + "id": "ter_9f2c1a", + "kind": "territory", + "label": "Hip actuator maintenance", + "summary": "Recurring inspection and repair work on the robot's hip actuator, spanning three sessions.", + "first_seen_at": "2026-07-01T08:14:00Z", + "last_seen_at": "2026-07-12T09:03:30Z", + "provenance": { "evidence_ids": ["ev_a41b", "ev_c9d0"] } + }, + "envelope": { } +} +``` + +| Field | Required | Meaning | +|---|---|---| +| `record.id` | **yes** | Stable identity. The same thing carries the same `id` across exports, forever. | +| `record.kind` | **yes** | What kind of record this is (§2). Consumers MUST skip unknown kinds, never fail on them. | +| `record.label` | no | Short handle for pointing at the record. | +| `record.summary` | no | Narrative description, meaning-preserving. | +| `record.provenance` | per-kind | How this record traces back to evidence (§2). Required on every kind except `evidence`. | +| `record.location` | no | The location object from the capture protocol (SPEC.md §3), round-tripped unchanged. | +| `record.exported_at` | no | When the engine wrote this version of the record. Later `exported_at` for the same `id` supersedes earlier. | +| `envelope` | no | Opaque engine-side metadata, safe to ignore. Never load-bearing. | + +Unknown fields are ignored by consumers (forward compatibility); +consumers MUST NOT rely on unknown fields being present. + +## 2. Record kinds + +Four kinds are defined in v1.1. The set is additive — future +versions add kinds, never remove or repurpose them. + +| Kind | What it is | Provenance shape | +|---|---|---| +| `evidence` | A captured observation, given back — the round-trip of intake. | none (it IS the evidence) | +| `territory` | A region of recurring meaning the engine found across evidence. | `evidence_ids` | +| `connection` | A typed link between two exported records. | `evidence_ids` | +| `reading` | A rendered narrative composed from the records it cites. | `record_ids` | + +**`evidence`** carries the capture fields back out, per turn: + +```json +{ "record": { + "id": "ev_a41b", + "kind": "evidence", + "thread_id": "robot-g1:session-042", + "source_kind": "oneshot_session", + "speaker": "operator", + "text": "has this joint clicked before?", + "occurred_at": "2026-07-12T09:01:00Z", + "location": { "provider": "named", "domain_id": "site-14", "anchor_id": "robot-g1", "label": "Loading dock" } +} } +``` + +**`connection`** links two records by `id`: + +```json +{ "record": { + "id": "con_77e0", + "kind": "connection", + "from_id": "ter_9f2c1a", + "to_id": "ter_b3d4e5", + "relation": "related", + "provenance": { "evidence_ids": ["ev_c9d0"] } +} } +``` + +`relation` is lowercase snake_case; consumers MUST tolerate relation +values they don't recognize. + +**`reading`** is prose composed from other records: + +```json +{ "record": { + "id": "read_2026-07-12", + "kind": "reading", + "label": "Daily read", + "body": "Maintenance work on the hip actuator continued; the clicking reported in June has not recurred since the re-torque.", + "composed_at": "2026-07-12T18:00:00Z", + "provenance": { "record_ids": ["ter_9f2c1a", "ev_a41b"] } +} } +``` + +## 3. The drop-file convention + +A file whose name ends in **`.bevia-export.jsonl`**, written by a +Bevia app into a folder the user chose, is an export: **one record +per line**, each line exactly the record envelope of §1. The mirror +of the intake drop-file — one contract, any transport. + +- **Atomic writes:** engines write to a temp name, then rename — + consumers may read on first sight of the suffix. +- **Repeat-safe reading:** re-reading any export file is always safe. + Consumers deduplicate by `record.id`; the version with the latest + `exported_at` (or, absent that, the latest-read occurrence) wins. +- **Snapshots and deltas:** a file may contain the whole map or just + what changed. The dedupe rule above makes the difference invisible + to a correct consumer. + +Any tool that can read a file is a consumer. No SDK, no network: +local engine to local file to local tool. + +## 4. The pull convention + +Engines that expose a network exit do so **pull-shaped**: the +consumer issues an HTTP `GET` with a bearer key the user created, +and receives records as JSON or streamed JSONL. The consumer chooses +the cadence — a cron job, a poll loop, an agent's question. + +- **Auth:** `Authorization: Bearer `. Keys are user-created and + user-revocable; revoking a key closes the door. +- **Rate limiting:** engines may answer `429` with a `Retry-After` + header. Consumers SHOULD honor it. +- **Pull, not push:** an engine never initiates delivery to a + destination the user did not explicitly configure. + +## 5. Behavioral guarantees + +What a consumer may rely on, stated as behavior: + +1. **Everything traces back.** Every record except `evidence` carries + provenance to the records or evidence that produced it. Nothing + comes out of the engine unattributed. +2. **Stable identity.** The same thing keeps the same `id` across + exports. Re-exporting is always safe; consumers dedupe by `id`. +3. **Event time is honored.** Time fields describe when things + happened or were observed in the user's timeline — never when the + engine processed them. +4. **No verdicts.** Records describe observed structure and carry its + evidence. They never contain scores, ratings, or judgments about a + person. +5. **The exit is never sold separately.** An engine that persists a + record makes it exportable. Export of the user's own records is + not metered or withheld behind additional payment. +6. **Location round-trips.** A capture that carried a location object + produces evidence that carries it back out, unchanged. + +## 6. Versioning + +This spec is versioned with the capture protocol and **additive-only**: +kinds and fields may be added; existing kinds, fields, requiredness, +and the guarantees in §5 do not change within v1.x. Breaking changes +require a v2 and a new suffix negotiation. + +## 7. Out of scope + +Engine behavior beyond §5; how records are computed; interpretation +of any kind; licensing of any Bevia engine. Implementing this spec — +as an engine or as a consumer — requires no license; running a Bevia +engine does. diff --git a/SPEC.md b/SPEC.md index 5cab6b9..9e0b65a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -21,18 +21,18 @@ evidence only: sensors capture, they never interpret. ```json { "capture": { - "thread_id": "terri-g1:session-042", + "thread_id": "robot-g1:session-042", "source_kind": "oneshot_session", - "source_platform": "shipmemory", + "source_platform": "example", "source_url": "https://example.com/optional-origin", "conversation": [ { "speaker": "operator", "text": "has this joint clicked before?", "emitted_at": "2026-07-12T09:01:00Z" }, { "speaker": "ai", "text": "Checking the manual…", "emitted_at": "2026-07-12T09:01:04Z" } ], "location": { - "provider": "posemesh", - "domain_id": "store-oslo-14", - "anchor_id": "terri-g1", + "provider": "named", + "domain_id": "site-14", + "anchor_id": "robot-g1", "label": "Loading dock" } }, @@ -48,7 +48,7 @@ evidence only: sensors capture, they never interpret. | `capture.conversation[].emitted_at` | no | **Event time** (RFC 3339): when this was said/observed — not when it was exported. | | `capture.thread_id` | no | Stable identity for the conversation/session. Same thread over time → same `thread_id`. | | `capture.source_kind` | no | What kind of source this is (see §5 naming). Sensors SHOULD set it explicitly. | -| `capture.source_platform` | no | The producing platform/tool, e.g. `shipmemory`. | +| `capture.source_platform` | no | The producing platform/tool, e.g. `my-capture-tool`. | | `capture.source_url` | no | Link back to the origin, if one exists. | | `capture.location` | no | Where the knowledge happened (§3). | | `envelope` | no | Opaque sensor-side metadata, carried through for observability. Never load-bearing. | @@ -80,7 +80,7 @@ Provider-shaped so any spatial system fits without schema changes. | Field | Meaning | |---|---| -| `provider` | Spatial system that produced the reading: `posemesh`, `named`, `gps`, … | +| `provider` | Spatial system that produced the reading: `gps`, `named`, or any anchor-based spatial protocol. | | `domain_id` | The space itself (e.g. one store, one ship, one site). | | `anchor_id` | Nearest anchor/asset/portal within the space. | | `pose` | `{x, y, z}` within the domain, in the provider's units. | @@ -122,7 +122,7 @@ within v1. Breaking changes require a v2 and a new suffix negotiation. ## 7. Out of scope -Engine behavior beyond §4; interpretation of any kind; query and export -surfaces (a separate export-format spec is planned as v1.1); licensing -of any Bevia engine. Implementing this spec requires no license; running -an engine does. +Engine behavior beyond §4; interpretation of any kind; licensing of +any Bevia engine. The exit door — how records come back out — is +specified separately in [SPEC-EXPORT.md](SPEC-EXPORT.md) (v1.1). +Implementing this spec requires no license; running an engine does. diff --git a/examples/robot-session.bevia-capture.json b/examples/robot-session.bevia-capture.json index 994cc80..c168758 100644 --- a/examples/robot-session.bevia-capture.json +++ b/examples/robot-session.bevia-capture.json @@ -1,17 +1,17 @@ { "capture": { - "thread_id": "terri-g1:session-042", + "thread_id": "robot-g1:session-042", "source_kind": "oneshot_session", - "source_platform": "shipmemory", + "source_platform": "example", "conversation": [ { "speaker": "operator", "text": "has this joint clicked before?", "emitted_at": "2026-07-12T09:01:00Z" }, { "speaker": "ai", "text": "Checking the manual for the hip actuator…", "emitted_at": "2026-07-12T09:01:04Z" }, { "speaker": "tool", "text": "{\"question\":\"actuator inspection step complete\",\"response\":\"confirmed\"}", "emitted_at": "2026-07-12T09:03:30Z" } ], "location": { - "provider": "posemesh", - "domain_id": "store-oslo-14", - "anchor_id": "terri-g1", + "provider": "named", + "domain_id": "site-14", + "anchor_id": "robot-g1", "label": "Loading dock" } } diff --git a/examples/robot-session.bevia-export.jsonl b/examples/robot-session.bevia-export.jsonl new file mode 100644 index 0000000..a6908d5 --- /dev/null +++ b/examples/robot-session.bevia-export.jsonl @@ -0,0 +1,5 @@ +{"record":{"id":"ev_a41b","kind":"evidence","thread_id":"robot-g1:session-042","source_kind":"oneshot_session","speaker":"operator","text":"has this joint clicked before?","occurred_at":"2026-07-12T09:01:00Z","location":{"provider":"named","domain_id":"site-14","anchor_id":"robot-g1","label":"Loading dock"}}} +{"record":{"id":"ev_c9d0","kind":"evidence","thread_id":"robot-g1:session-042","source_kind":"oneshot_session","speaker":"ai","text":"Checking the manual for the hip actuator…","occurred_at":"2026-07-12T09:01:04Z","location":{"provider":"named","domain_id":"site-14","anchor_id":"robot-g1","label":"Loading dock"}}} +{"record":{"id":"ter_9f2c1a","kind":"territory","label":"Hip actuator maintenance","summary":"Recurring inspection and repair work on the robot's hip actuator, spanning three sessions.","first_seen_at":"2026-07-01T08:14:00Z","last_seen_at":"2026-07-12T09:03:30Z","exported_at":"2026-07-12T18:00:00Z","provenance":{"evidence_ids":["ev_a41b","ev_c9d0"]}}} +{"record":{"id":"con_77e0","kind":"connection","from_id":"ter_9f2c1a","to_id":"ter_b3d4e5","relation":"related","provenance":{"evidence_ids":["ev_c9d0"]}}} +{"record":{"id":"read_2026-07-12","kind":"reading","label":"Daily read","body":"Maintenance work on the hip actuator continued; the clicking reported in June has not recurred since the re-torque.","composed_at":"2026-07-12T18:00:00Z","provenance":{"record_ids":["ter_9f2c1a","ev_a41b"]}}} diff --git a/reference/README.md b/reference/README.md index 35ef048..e03ec56 100644 --- a/reference/README.md +++ b/reference/README.md @@ -2,10 +2,10 @@ Implementations of this protocol, usable as-is or as starting points: -- **bevia-spatial-intake** — thin intake clients: a Mentra-glasses - capture scaffold and a ShipMemory/Auki bridge poller that lands - robotics assets' ContextCards and session history as captures. - (Link published when the repo is made public.) +- **Reference intake clients** — thin sensors for spatial and + robotics capture sources: a wearable-capture scaffold and a bridge + poller that lands device session history and context as captures. + (Links published when the repos are made public.) - **An iOS Shortcut, a shell script, a cron job** — any tool that can write a `*.bevia-capture.json` file into a watched folder is a complete sensor. No SDK required; that is the point of the protocol. diff --git a/schema/capture.schema.json b/schema/capture.schema.json index 1f8793a..15093b8 100644 --- a/schema/capture.schema.json +++ b/schema/capture.schema.json @@ -22,7 +22,7 @@ }, "source_platform": { "type": "string", - "description": "The producing platform/tool (e.g. shipmemory)." + "description": "The producing platform/tool (e.g. my-capture-tool)." }, "source_url": { "type": "string", diff --git a/schema/export-record.schema.json b/schema/export-record.schema.json new file mode 100644 index 0000000..7ba7975 --- /dev/null +++ b/schema/export-record.schema.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://bevia.co/schema/export-record.schema.json", + "title": "Bevia Export Protocol v1.1 — export record", + "description": "One exported record. In a *.bevia-export.jsonl drop-file, each line is exactly this object. Consumers MUST skip records whose kind they don't recognize, and MUST ignore unknown fields.", + "type": "object", + "required": ["record"], + "additionalProperties": true, + "properties": { + "record": { + "type": "object", + "required": ["id", "kind"], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "Stable identity. The same thing carries the same id across exports, forever. Consumers deduplicate by id." + }, + "kind": { + "type": "string", + "description": "Record kind. v1.1 defines: evidence, territory, connection, reading. The set is additive; unknown kinds are skipped, never a failure." + }, + "label": { + "type": "string", + "description": "Short handle for pointing at the record." + }, + "summary": { + "type": "string", + "description": "Narrative description, meaning-preserving." + }, + "exported_at": { + "type": "string", + "format": "date-time", + "description": "When the engine wrote this version. Later exported_at for the same id supersedes earlier." + }, + "provenance": { + "type": "object", + "additionalProperties": true, + "description": "How this record traces back. Required on every kind except evidence.", + "properties": { + "evidence_ids": { + "type": "array", + "items": { "type": "string" }, + "description": "Ids of the evidence records that produced this record." + }, + "record_ids": { + "type": "array", + "items": { "type": "string" }, + "description": "Ids of the records a reading composes from." + } + } + }, + "location": { + "$ref": "https://bevia.co/schema/location.schema.json" + }, + "thread_id": { + "type": "string", + "description": "evidence: the thread this observation belongs to, from the original capture." + }, + "source_kind": { + "type": "string", + "description": "evidence: kind of source, from the original capture." + }, + "speaker": { + "type": "string", + "description": "evidence: contributor label, from the original capture." + }, + "text": { + "type": "string", + "description": "evidence: the observed content, verbatim." + }, + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "evidence: event time — when this happened in the user's timeline." + }, + "first_seen_at": { + "type": "string", + "format": "date-time", + "description": "territory: earliest evidence, in event time." + }, + "last_seen_at": { + "type": "string", + "format": "date-time", + "description": "territory: latest evidence, in event time." + }, + "from_id": { + "type": "string", + "description": "connection: id of the record the link starts from." + }, + "to_id": { + "type": "string", + "description": "connection: id of the record the link points to." + }, + "relation": { + "type": "string", + "description": "connection: lowercase snake_case link type. Consumers tolerate unknown values." + }, + "body": { + "type": "string", + "description": "reading: the composed narrative." + }, + "composed_at": { + "type": "string", + "format": "date-time", + "description": "reading: when the narrative was composed." + } + } + }, + "envelope": { + "type": "object", + "description": "Opaque engine-side metadata, safe to ignore. Never load-bearing.", + "additionalProperties": true + } + } +} diff --git a/schema/location.schema.json b/schema/location.schema.json index 9f79ca4..f1fc862 100644 --- a/schema/location.schema.json +++ b/schema/location.schema.json @@ -8,7 +8,7 @@ "properties": { "provider": { "type": "string", - "description": "Spatial system that produced the reading: posemesh, named, gps, …" + "description": "Spatial system that produced the reading: gps, named, or any anchor-based spatial protocol." }, "domain_id": { "type": "string",