The language-neutral contract every RunnerQ implementation agrees on. It holds:
- the constants they share;
- golden vectors for the logic each SDK implements on its own;
- the Postgres schema;
- the storage protocol between workers and the data plane;
- the conductor protocol between workers' agents and the control plane;
- a cross-language conformance suite, which also runs Go and TypeScript against one database.
There is no runtime code here, only definitions, a generator and test data.
Consumers: runnerq-go, runnerq-ts, cloud-storage-go, cloud-storage-ts and RunnerQ Cloud.
Go and TypeScript implement RunnerQ separately against one Postgres schema and two wire protocols. Until now, parity was kept by hand:
- Schema: Go owns the DDL, and TypeScript re-declares it and validates the catalog.
- Storage protocol: it is regex-generated from Go source and mirrored by hand in TypeScript.
- Conductor protocol: there are three hand copies of a prose spec.
- Pure logic: functions like checkpoint IDs and idempotency keys are written twice and pinned by a few golden values.
This repo makes each of those a single source. The idea follows Temporal's
coresdk protos and features repo, without shipping a shared native core.
runnerq-spec/
├── VERSION spec release (semver)
├── VERSIONING.md compatibility rules
├── CHANGELOG.md
├── constants/
│ └── constants.json lock key, channel and key prefixes,
│ serialization ids, storage error kinds
├── vectors/ golden inputs → outputs for pure logic
│ ├── checkpoint_id.json
│ ├── business_key.json
│ ├── application_key.json
│ ├── step_key.json
│ ├── attempts_remain.json retry decision
│ ├── retry_delay.json backoff
│ ├── canonical_status.json
│ ├── canonical_event.json
│ ├── internal_events.json
│ ├── index_definition.json catalog normalization
│ └── column_default.json
├── schema/
│ └── postgres/ migrations, concurrent indexes, catalog
│ (see its README)
├── protocol/
│ ├── storage/ storage operations, as JSON Schema with
│ │ examples (see its README)
│ └── conductor/ agent ↔ Cloud messages and hosted worker
│ reports, as JSON Schema with examples
├── conformance/ scenarios every SDK's storage must pass, and
│ the driver protocol (see its README)
├── schemacheck/ validates JSON against the schemas
├── serialization/
│ ├── formats.md json-v1, superjson-v1
│ └── vectors/
│ └── plain_json.json
├── verify/ reference implementation; checks every vector
└── tools/
├── gen/ constants, schema and protocols → Go / TypeScript
├── catalog/ schema → catalog.json, from a real Postgres
└── conformance/ runs the scenarios against SDK drivers
Later phases add these directories:
schema/postgres/functions/, possibly, for SQL state-transition functions.conformance/for data-driven scenarios and the mixed Go/TS fleet test.
Every vector file has the same shape:
{
"description": "The rule, precisely enough to implement from.",
"cases": [
{ "name": "unique case name", "input": { ... }, "output": ... }
]
}Rules:
- Outputs compare as JSON values.
verify/implements each description from scratch using only Go's standard library, and fails if any output disagrees.- The UUID and base64 vectors have also been checked against Python's
uuid.uuid5andbase64.
To add a case:
- Add it with
"output": null. - Run
go test ./verify -updateto fill in the output. - Check the filled-in value by some independent means before committing.
go run ./tools/gen -lang go -pkg spec -out <file.go> # constants
go run ./tools/gen -lang ts -out <file.ts>
go run ./tools/gen -lang go -pkg spec -part schema -out <file.go> # Postgres schema
go run ./tools/gen -lang ts -part schema -out <file.ts>
go run ./tools/gen -part storage -lang go-protocol|go-client|go-server|ts -pkg <pkg> -out <file>
go run ./tools/gen -part conductor -lang go|ts|ts-specs -pkg <pkg> -out <file>
go run ./tools/gen -part executor-report -lang go|ts -pkg <pkg> -out <file>The storage protocol's Go code refers to runnerq-go's storage types
(-storage-import), which the schema names with x-go-type.
Go names are PascalCase with Go initialisms (SerializationJSON); TypeScript
names are camelCase (serializationJson). Enums become numbered constants in Go
and an as const object in TypeScript. 64-bit integers are decimal strings in
TypeScript.
- Add this repo as a git submodule at
spec/, pinned to a release tag. - Generate constants from the submodule into the consumer, and commit the output.
- runnerq-go:
go generate ./internal/spec - runnerq-ts:
npm run spec:genBecause the output is committed,go getand npm-from-git users never need the submodule.
- runnerq-go:
- Tests read the vectors straight from
spec/. - CI checks out submodules, regenerates the constants, and fails on any diff.
Bumping the spec in a consumer is one commit: the submodule pointer, the regenerated files and any code changes.
Everything is JSON. The generator and the verifier need only Go's standard library. Both wire protocols are JSON, and protocol v1 must stay byte-identical: the storage protocol uses Go's default JSON, with capitalized field names and durations in nanoseconds. So the protocols will be described with JSON Schema. Protobuf's JSON mapping would rename fields and re-encode durations, which would force a v2.
| Phase | Scope | Status |
|---|---|---|
| 1 | Constants, pure-logic vectors, generator; both SDKs use the constants and test against the vectors | v0.1.0 |
| 2 | Extract backoff and the retry decision into pure functions; vectors for them and the status and event mappings | v0.2.0 |
| 3 | Schema: migrations, concurrent indexes and catalog; both SDKs migrate and validate from it | v0.3.0 |
| 4a | Storage protocol and executor reports; generated code replaces generate.py |
v0.4.0 |
| 4b | Conductor protocol; generated types replace the hand copies | v0.5.0 |
| 5 | Conformance scenarios, Go and TS drivers, mixed-fleet job, storaged protocol replay | v0.6.0: scenarios, runner, drivers |
| 6 | Decide on SQL functions for state transitions |
go vet ./... && go test ./...CI also checks that the generator's output for both languages builds and is formatted.