Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 76 additions & 16 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,78 @@
# Protocol conformance

Conformance fixtures give every Murmur SDK the same observable behavior. They
use the ProtoJSON field names and enum values defined in `spec/`.

An SDK is conformant for a fixture set when it can:

1. parse every line without losing the protocol, session, sequence, timing, or
payload variant;
2. reject an unknown payload variant rather than silently treating it as a
command;
3. preserve 64-bit integer values even on runtimes whose JSON number type
cannot represent them safely;
4. serialize an equivalent ProtoJSON object; object key order is irrelevant;
5. keep transcript text and audio out of diagnostics produced during parsing.

Fixtures contain synthetic text only. Real recordings and conversations do not
belong in this directory.
The JSON Lines fixtures give every Murmur SDK the same observable behavior.
They use the ProtoJSON field and enum names defined in `spec/`, contain only
synthetic data, and must never contain recordings, conversations, credentials,
or real identifiers.

## Manifest and runner contract

`manifest.json` is the only fixture index. `manifestVersion` identifies this
manifest shape; `protocol` identifies the current wire contract. Every member
of `fixtureSets` supplies a unique `name`, an explicit `message`, a path, line
count, and either `expect: accept` or `expect: reject`.

Reject sets also declare a stable `reason`, a `rejection` phase (`parse` or
`order`), and optionally `rejectLine` (one-based, default 1). For parse
rejections every line at or after `rejectLine` must fail parsing. For order
rejections every line must parse, lines before `rejectLine` must strictly
increase, and each line from `rejectLine` onward must fail only that ordering
predicate. Reason values are `missing-payload`, `ambiguous-oneof`,
`missing-session-command`, `ambiguous-session-command`, `invalid-enum`,
`sequence-order`, `invalid-uint64`, `unsupported-protocol-major`,
`invalid-protocol-version`, and `invalid-audio-frame`.

Accept sets may declare slash-separated `unknownFields`. A runner parses each
line, computes its ordering key (`sequence` for RuntimeEvent and AudioFrame,
`requestSequence` for SessionControl, none for VoiceSource), and checks the
expected result. Accepted values are serialized and compared structurally with
the input after every declared unknown path has been removed from both values.
Parse success is asserted separately. Failure messages use
`<sdk> · <set> · line N · <reason or error>` and round-trip failures end in
`· round-trip`.

Unknown additive fields beside a known oneof arm are ignored for a supported
major. A message whose only arm is unknown is rejected because it has no known
arm. Accepted fixtures are canonical: default-valued fields are omitted.

## Compatibility profile

| Concern | Wire (protobuf / ProtoJSON) | Murmur profile (fixture-enforced) | SDK policy |
| --- | --- | --- | --- |
| uint64 | ProtoJSON parses integer numbers or strings, including exponent forms | Serialize decimal strings; string input is ASCII digits only and at most 2^64-1 | SDKs may additionally accept exact, non-negative JSON integers; in-memory type is language-specific |
| uint32 | JSON integer number | Number must be an integer, not a boolean, from 0 through 2^32-1 | Shared helper and language-specific error type |
| protocol | Version fields are uint32 | Major must equal 1; every non-negative uint32 minor is accepted | Envelope parsers always enforce support |
| oneof | One selected arm | Exactly one known arm is required | Error representation is language-specific |
| unknown fields | Rejected by ProtoJSON parsers by default | Ignored within a supported major | SDKs need not preserve them |
| enums | ProtoJSON accepts names and integers | Validated fields accept known string names only; integers are rejected | Unvalidated opaque bodies remain unchanged |
| bytes | Base64 string | Standard or URL-safe base64 grammar below | SDKs keep the encoded string and need not decode bytes |
| ordering | Application concern | Keys in an accepted set must strictly increase | No sequence-tracking API is required |
| defaults | Usually emitted implicitly by binary encoding | Omit defaults in canonical JSON; VoiceSource omits empty capabilities and metadata | SDKs may always emit required envelope fields |

Validated RuntimeEvent fields are transcript `kind` and `text`, audio-level
`amplitude` in [0, 1], and session-state `previous` and `current`. SessionControl
validates optional input-gate booleans, optional stop reason, and optional start
mode, source, and requested format. AudioFrame validates its format and payload.
VoiceSource requires non-empty identifiers and display names, a known transport,
known capabilities, and string-to-string metadata. Intent, confirmation,
action-result, error, and all other body data remain opaque and are echoed.

Base64 validation counts trailing `=` characters as padding (at most two).
After stripping padding, a length remainder of one modulo four is invalid. If
padding is present it must equal `(4 - remainder) % 4`, and total length must be
divisible by four. The unpadded content must use either the standard alphabet
or the URL-safe alphabet, never a mixture. The empty string is valid.

## Synthetic audio

Both audio fixtures are 16,000 Hz, mono, `AUDIO_ENCODING_PCM_S16LE`, and 10 ms:
160 samples and 320 bytes. The first is a 1 kHz sine wave with sample `n` equal
to `round(8000 * sin(2*pi*1000*n/16000))`, encoded little-endian signed int16.
The second is 320 zero bytes. Recreate the first payload with:

```sh
python3 -c "import base64,math,struct; print(base64.b64encode(b''.join(struct.pack('<h',round(8000*math.sin(2*math.pi*1000*n/16000))) for n in range(160))).decode())"
```

The repository checker decodes fixture base64 and verifies PCM byte length;
SDK parsers validate only the base64 grammar and format metadata.
2 changes: 2 additions & 0 deletions conformance/fixtures/audio-frames.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-test-2","sequence":"1","monotonicTimeUs":"10000","format":{"sampleRateHz":16000,"channels":1,"encoding":"AUDIO_ENCODING_PCM_S16LE","frameDurationMs":10},"payload":"AAD1CxkW3xxAH98cGRb1CwAAC/Tn6SHjwOAh4+fpC/QAAPULGRbfHEAf3xwZFvULAAAL9OfpIePA4CHj5+kL9AAA9QsZFt8cQB/fHBkW9QsAAAv05+kh48DgIePn6Qv0AAD1CxkW3xxAH98cGRb1CwAAC/Tn6SHjwOAh4+fpC/QAAPULGRbfHEAf3xwZFvULAAAL9OfpIePA4CHj5+kL9AAA9QsZFt8cQB/fHBkW9QsAAAv05+kh48DgIePn6Qv0AAD1CxkW3xxAH98cGRb1CwAAC/Tn6SHjwOAh4+fpC/QAAPULGRbfHEAf3xwZFvULAAAL9OfpIePA4CHj5+kL9AAA9QsZFt8cQB/fHBkW9QsAAAv05+kh48DgIePn6Qv0AAD1CxkW3xxAH98cGRb1CwAAC/Tn6SHjwOAh4+fpC/Q="}
{"protocol":{"major":1,"minor":0},"sessionId":"session-test-2","sequence":"2","monotonicTimeUs":"20000","format":{"sampleRateHz":16000,"channels":1,"encoding":"AUDIO_ENCODING_PCM_S16LE","frameDurationMs":10},"payload":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}
3 changes: 3 additions & 0 deletions conformance/fixtures/forward-compatible-events.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"protocol":{"major":1,"minor":1},"sessionId":"session-test-forward","sequence":"1","monotonicTimeUs":"1000","captureReadiness":{"live":true},"extension":{"synthetic":true}}
{"protocol":{"major":1,"minor":1},"sessionId":"session-test-forward","sequence":"2","monotonicTimeUs":"2000","transcript":{"kind":"TRANSCRIPT_KIND_FINAL","text":"synthetic forward-compatible text","speakerLabel":"synthetic-speaker"}}
{"protocol":{"major":1,"minor":1},"sessionId":"session-test-forward","sequence":"18446744073709551615","monotonicTimeUs":"18446744073709551615","audioLevel":{"amplitude":0.5}}
1 change: 1 addition & 0 deletions conformance/fixtures/invalid/ambiguous-oneof.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"1","monotonicTimeUs":"1","captureReadiness":{"live":true},"audioLevel":{"amplitude":0.5}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","requestSequence":"1","start":{},"stop":{}}
5 changes: 5 additions & 0 deletions conformance/fixtures/invalid/audio-frames.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"1","monotonicTimeUs":"1","format":{"sampleRateHz":16000,"channels":1,"encoding":"AUDIO_ENCODING_MP3","frameDurationMs":10},"payload":"AAAA"}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"2","monotonicTimeUs":"2","format":{"sampleRateHz":16000,"channels":1,"encoding":"AUDIO_ENCODING_PCM_S16LE","frameDurationMs":10},"payload":"not*base64"}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"3","monotonicTimeUs":"3","format":{"sampleRateHz":16000,"channels":1,"encoding":"AUDIO_ENCODING_PCM_S16LE","frameDurationMs":10},"payload":"AAA=="}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"4","monotonicTimeUs":"4","format":{"sampleRateHz":"16000","channels":1,"encoding":"AUDIO_ENCODING_PCM_S16LE","frameDurationMs":10},"payload":"AAAA"}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"5","monotonicTimeUs":"5","format":{"sampleRateHz":16000,"channels":1,"encoding":"AUDIO_ENCODING_PCM_S16LE","frameDurationMs":10}}
2 changes: 2 additions & 0 deletions conformance/fixtures/invalid/enum.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"1","monotonicTimeUs":"1","transcript":{"kind":"TRANSCRIPT_KIND_SYNTHETIC_BOGUS","text":"synthetic invalid enum"}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"2","monotonicTimeUs":"2","sessionStateChanged":{"previous":"SESSION_STATE_IDLE","current":"SESSION_STATE_SYNTHETIC_BOGUS"}}
1 change: 1 addition & 0 deletions conformance/fixtures/invalid/missing-payload.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"1","monotonicTimeUs":"1"}
1 change: 1 addition & 0 deletions conformance/fixtures/invalid/missing-session-command.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","requestSequence":"1"}
2 changes: 2 additions & 0 deletions conformance/fixtures/invalid/protocol-major.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"protocol":{"major":2,"minor":0},"sessionId":"session-invalid","sequence":"1","monotonicTimeUs":"1","captureReadiness":{"live":true}}
{"protocol":{"major":0,"minor":0},"sessionId":"session-invalid","sequence":"2","monotonicTimeUs":"2","captureReadiness":{"live":true}}
3 changes: 3 additions & 0 deletions conformance/fixtures/invalid/protocol-version.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"protocol":{"major":1.5,"minor":0},"sessionId":"session-invalid","sequence":"1","monotonicTimeUs":"1","captureReadiness":{"live":true}}
{"protocol":{"major":true,"minor":0},"sessionId":"session-invalid","sequence":"2","monotonicTimeUs":"2","captureReadiness":{"live":true}}
{"protocol":{"major":1,"minor":-1},"sessionId":"session-invalid","sequence":"3","monotonicTimeUs":"3","captureReadiness":{"live":true}}
4 changes: 4 additions & 0 deletions conformance/fixtures/invalid/sequence-order.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid-order","sequence":"1","monotonicTimeUs":"1","captureReadiness":{"live":true}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid-order","sequence":"3","monotonicTimeUs":"2","captureReadiness":{"live":true}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid-order","sequence":"2","monotonicTimeUs":"3","captureReadiness":{"live":true}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid-order","sequence":"3","monotonicTimeUs":"4","captureReadiness":{"live":true}}
4 changes: 4 additions & 0 deletions conformance/fixtures/invalid/uint64.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"18446744073709551616","monotonicTimeUs":"1","captureReadiness":{"live":true}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"+1","monotonicTimeUs":"2","captureReadiness":{"live":true}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"-1","monotonicTimeUs":"3","captureReadiness":{"live":true}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-invalid","sequence":"١","monotonicTimeUs":"4","captureReadiness":{"live":true}}
5 changes: 5 additions & 0 deletions conformance/fixtures/session-control.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{"protocol":{"major":1,"minor":0},"sessionId":"session-test-2","requestSequence":"1","start":{"source":{"sourceId":"source-synthetic-1","displayName":"Synthetic microphone","transport":"SOURCE_TRANSPORT_SYNTHETIC","capabilities":["SOURCE_CAPABILITY_LIVE_AUDIO","SOURCE_CAPABILITY_INPUT_MUTE"],"metadata":{"manufacturer":"synthetic","model":"fixture"}},"mode":"CAPTURE_MODE_HOLD_TO_TALK","requestedFormat":{"sampleRateHz":16000,"channels":1,"encoding":"AUDIO_ENCODING_PCM_S16LE","frameDurationMs":10}}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-test-2","requestSequence":"2","inputGate":{"open":true}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-test-2","requestSequence":"3","inputGate":{"open":false,"flushAcceptedAudio":true}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-test-2","requestSequence":"4","finalize":{}}
{"protocol":{"major":1,"minor":0},"sessionId":"session-test-2","requestSequence":"5","stop":{"reason":"synthetic-test"}}
3 changes: 3 additions & 0 deletions conformance/fixtures/source-discovery.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"sourceId":"source-synthetic-1","displayName":"Synthetic microphone","transport":"SOURCE_TRANSPORT_SYNTHETIC","capabilities":["SOURCE_CAPABILITY_LIVE_AUDIO","SOURCE_CAPABILITY_INPUT_MUTE"],"metadata":{"manufacturer":"synthetic","model":"fixture"}}
{"sourceId":"source-synthetic-minimal","displayName":"Synthetic minimal source","transport":"SOURCE_TRANSPORT_LOCAL_AUDIO"}
{"sourceId":"synthetic","displayName":"Synthetic source","transport":"SOURCE_TRANSPORT_SYNTHETIC","metadata":{"__proto__":"value","normal":"kept"}}
22 changes: 16 additions & 6 deletions conformance/manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
{
"manifestVersion": 1,
"protocol": { "major": 1, "minor": 0 },
"fixtureSets": [
{
"name": "runtime-events",
"path": "fixtures/runtime-events.jsonl",
"events": 7,
"synthetic": true
}
{ "name": "runtime-events", "message": "RuntimeEvent", "path": "fixtures/runtime-events.jsonl", "lines": 7, "expect": "accept" },
{ "name": "source-discovery", "message": "VoiceSource", "path": "fixtures/source-discovery.jsonl", "lines": 3, "expect": "accept" },
{ "name": "session-control", "message": "SessionControl", "path": "fixtures/session-control.jsonl", "lines": 5, "expect": "accept" },
{ "name": "audio-frames", "message": "AudioFrame", "path": "fixtures/audio-frames.jsonl", "lines": 2, "expect": "accept" },
{ "name": "forward-compatible-events", "message": "RuntimeEvent", "path": "fixtures/forward-compatible-events.jsonl", "lines": 3, "expect": "accept", "unknownFields": ["extension", "transcript/speakerLabel"] },
{ "name": "invalid-missing-payload", "message": "RuntimeEvent", "path": "fixtures/invalid/missing-payload.jsonl", "lines": 1, "expect": "reject", "reason": "missing-payload", "rejection": "parse" },
{ "name": "invalid-ambiguous-oneof", "message": "RuntimeEvent", "path": "fixtures/invalid/ambiguous-oneof.jsonl", "lines": 1, "expect": "reject", "reason": "ambiguous-oneof", "rejection": "parse" },
{ "name": "invalid-missing-session-command", "message": "SessionControl", "path": "fixtures/invalid/missing-session-command.jsonl", "lines": 1, "expect": "reject", "reason": "missing-session-command", "rejection": "parse" },
{ "name": "invalid-ambiguous-session-command", "message": "SessionControl", "path": "fixtures/invalid/ambiguous-session-command.jsonl", "lines": 1, "expect": "reject", "reason": "ambiguous-session-command", "rejection": "parse" },
{ "name": "invalid-enum", "message": "RuntimeEvent", "path": "fixtures/invalid/enum.jsonl", "lines": 2, "expect": "reject", "reason": "invalid-enum", "rejection": "parse" },
{ "name": "invalid-sequence-order", "message": "RuntimeEvent", "path": "fixtures/invalid/sequence-order.jsonl", "lines": 4, "expect": "reject", "reason": "sequence-order", "rejection": "order", "rejectLine": 3 },
{ "name": "invalid-uint64", "message": "RuntimeEvent", "path": "fixtures/invalid/uint64.jsonl", "lines": 4, "expect": "reject", "reason": "invalid-uint64", "rejection": "parse" },
{ "name": "invalid-protocol-major", "message": "RuntimeEvent", "path": "fixtures/invalid/protocol-major.jsonl", "lines": 2, "expect": "reject", "reason": "unsupported-protocol-major", "rejection": "parse" },
{ "name": "invalid-protocol-version", "message": "RuntimeEvent", "path": "fixtures/invalid/protocol-version.jsonl", "lines": 3, "expect": "reject", "reason": "invalid-protocol-version", "rejection": "parse" },
{ "name": "invalid-audio-frames", "message": "AudioFrame", "path": "fixtures/invalid/audio-frames.jsonl", "lines": 5, "expect": "reject", "reason": "invalid-audio-frame", "rejection": "parse" }
]
}
8 changes: 4 additions & 4 deletions sdks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ APIs.

| SDK | Intended environments | State |
| --- | --- | --- |
| Dart (`murmur_protocol`) | Flutter and pure-Dart applications | Source and event model implemented; pub.dev metadata prepared |
| TypeScript (`@october-dev/murmur-protocol`) | Node, Electron, React Native, and web | Event model implemented; npm build metadata prepared |
| Python | Research, automation, and AI services | Event model implemented |
| Rust | Native runtimes, sidecars, servers, and embedded hosts | Event model implemented |
| Dart (`murmur_protocol`) | Flutter and pure-Dart applications | Event, session-control, audio-frame, and source models implemented; pub.dev metadata prepared |
| TypeScript (`@october-dev/murmur-protocol`) | Node, Electron, React Native, and web | Event, session-control, audio-frame, and source models implemented; npm build metadata prepared |
| Python | Research, automation, and AI services | Event, session-control, audio-frame, and source models implemented |
| Rust | Native runtimes, sidecars, servers, and embedded hosts | Event, session-control, audio-frame, and source models implemented |

The SDKs intentionally start small. Audio processing, BLE access, databases,
and model providers belong in separate packages built on these contracts.
Expand Down
2 changes: 2 additions & 0 deletions sdks/dart/murmur_protocol/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

- Add versioned protocol, runtime-event, and voice-source models.
- Add shared ProtoJSON conformance coverage.
- Expand conformance coverage for sessions, audio frames, and source discovery;
RuntimeEvent uint64 fields now use `BigInt`, and protocol support is major-only.
1 change: 1 addition & 0 deletions sdks/dart/murmur_protocol/lib/murmur_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ library;

export 'src/events.dart';
export 'src/protocol.dart';
export 'src/session.dart';
export 'src/source.dart';
Loading
Loading