mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
refactor: move the protocol schema to protocol/schema.json (RL-09)
The WebSocket message-type schema is the one artifact in this repository that neither component owns: `Server/ws/message_types.go` and `Client/src/lib/protocolTypes.ts` are both generated from it, and neither may be hand-edited. It nonetheless lived at `docs/protocol-schema.json` — filed under the directory for prose, whose own README calls it "Reference" material — and its generator lived at `Server/scripts/genprotocol/`, i.e. inside one of the two consumers. Ownership was legible from neither location. The obvious fix — move the generator to the repository root alongside the schema, so the whole tool is at the cross-component boundary — is wrong here. The generator is a Go `package main`, and Go modules are directory-rooted: `Server/go.mod` roots at `Server/`, so a root-level Go program needs a second module or a `go.work`. That second module would sit outside every path filter this repository already has — `golangci-lint` runs with `working-directory: Server/` (ci.yml), `go vet ./...` runs from `Server/` (scripts/run.mjs, .githooks/pre-commit), `.githooks/pre-commit` selects Go files with `^Server/.*\.go$`, `.githooks/pre-push` sets `server_changed` on `^Server/`, setup-go caches on `Server/go.sum`, and dependabot has one gomod block for `/Server`. Six gates would silently stop covering the generator, each failing open. The schema is data and moves freely; the generator is Go and stays where the Go toolchain already runs. Done instead: - `docs/protocol-schema.json` -> `protocol/schema.json`. A new top-level `protocol/` is the cross-component boundary, with a `README.md` naming the two generated consumers, the one command, and the four gates. - `Server/scripts/genprotocol/` -> `Server/cmd/genprotocol/`, the module's conventional home for an executable. This also empties `Server/scripts/` of Go entry points except `seed.go`, which RL-10 moves next. - `Server/cmd/` added to `Server/.dockerignore` and `Server/.air.toml`, which both already excluded `Server/scripts/`. Without this the move would have silently widened the Docker build context and the air watch set. 27 files, 115 insertions, 76 deletions. Two runtime path resolvers re-pointed (`cmd/genprotocol/main.go:41` `-schema` default, `ws/protocol_contract_test.go:67` `filepath.Join`); two git-hook grep patterns (`pre-commit:53`, `pre-push:57`); eight generator call sites across five files (Makefile x2, scripts/run.mjs x2, pre-commit x2, ci-check skill, bughunt-fix.js); two broken relative markdown links (docs/README.md:47, docs/protocol.md:1497); two generated files regenerated, header lines only, zero constants changed; two ledger prose hits plus a `render-ledger.mjs` re-render. No new verify was written: the regenerate-and-diff check is already enforced three times (CI `make protocol-verify`, `.githooks/pre-commit`, `npm run check:server`) and `ws/protocol_contract_test.go` independently checks the schema against the constants a fourth time. Verified: both directions, for both resolvers. With `protocol/schema.json` removed, `go test ./ws/ -run TestProtocol` fails with `reading protocol schema at /home/user/OwnCord/protocol/schema.json: no such file or directory` (two tests) and `go run ./cmd/genprotocol` exits 1 with `read schema: open ../protocol/schema.json: no such file or directory`; with the file restored both pass. So the new path is genuinely resolved, not merely spelled in a comment. The hook patterns were exercised directly: the pre-commit pattern matches `protocol/schema.json` and `Server/cmd/genprotocol/main.go` and no longer matches `docs/protocol-schema.json`; the pre-push pattern matches `protocol/schema.json`. `go run ./cmd/genprotocol` twice in a row leaves `git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts` clean, so the committed outputs are exactly what the generator emits. `go build ./...` and `go vet ./...` pass; `npx prettier --check .`, `npm run typecheck` and `npm run lint` pass; `node .superpowers/render-ledger.mjs --check` reports 348 findings valid. Not included: the four dated `docs/audit-*.md` files, the older `docs/plans/*`, and `CHANGELOG.md` keep the old path — they are point-in-time records, and `.prettierignore` and `scripts/check-doc-counts.mjs` already treat them as deliberately unmaintained. The B1 plan itself keeps its own wording, since it states intent rather than current state. `Server/scripts/` is not deleted: it still holds `seed.go` (RL-10), `k6/`, `toxiproxy/` and two shell scripts. `Server/telemetry/metrics.go:19` declares a scope for a `Server/voice` package that does not exist — spotted here, unrelated to this move, left for RL-13's sweep to carry forward verbatim rather than fixed inside a relocation. No `seed:` Make target was added. Refs RL-09, L-09
This commit is contained in:
@@ -37,7 +37,7 @@ golangci-lint run # CI pins v2.11.3
|
||||
# Generated output must not be stale. These are what `make sqlc-verify` and
|
||||
# `make protocol-verify` reduce to — make is not on PATH on a stock Windows box.
|
||||
sqlc generate && git diff --exit-code db/dbgen
|
||||
go run ./scripts/genprotocol && git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts
|
||||
go run ./cmd/genprotocol && git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts
|
||||
```
|
||||
|
||||
Add `-tags wazero` to `go vet`/`go test` when you touched `plugin/`.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
name: protocol-change
|
||||
description: Add or change a WebSocket message type in OwnCord. Use before editing docs/protocol-schema.json, Server/ws/message_types.go, or Client/src/lib/protocolTypes.ts.
|
||||
description: Add or change a WebSocket message type in OwnCord. Use before editing protocol/schema.json, Server/ws/message_types.go, or Client/src/lib/protocolTypes.ts.
|
||||
---
|
||||
|
||||
# protocol-change
|
||||
|
||||
`docs/protocol-schema.json` is the source of truth. Both constant files are
|
||||
generated from it by `Server/scripts/genprotocol/`.
|
||||
`protocol/schema.json` is the source of truth. Both constant files are
|
||||
generated from it by `Server/cmd/genprotocol/`.
|
||||
|
||||
**The schema holds message-type NAMES only.** Route by what you are changing —
|
||||
most payload work never touches it, and sending a field change through the
|
||||
@@ -23,7 +23,7 @@ forwards the message raw, there is nothing to add. If it **re-serialises**, an
|
||||
older server drops unknown JSON fields — so a field the server must forward is
|
||||
NOT backward compatible with older servers.
|
||||
|
||||
1. Edit `docs/protocol-schema.json`.
|
||||
1. Edit `protocol/schema.json`.
|
||||
2. Run `make protocol-generate` from `Server/`.
|
||||
3. Commit **both** outputs — `Server/ws/message_types.go` and
|
||||
`Client/src/lib/protocolTypes.ts`. One run regenerates the
|
||||
|
||||
@@ -506,7 +506,7 @@ const GATE_COMMANDS = {
|
||||
` make sqlc-verify protocol-verify # generated output must not be stale. If make is not on PATH, ` +
|
||||
`run the equivalent commands directly instead: ` +
|
||||
`"sqlc generate && git diff --exit-code db/dbgen" and ` +
|
||||
`"go run ./scripts/genprotocol && git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts" ` +
|
||||
`"go run ./cmd/genprotocol && git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts" ` +
|
||||
`- a non-empty diff in either means generated code is stale and the gate fails`,
|
||||
rust:
|
||||
`From Client/src-tauri:\n` + ` cargo test\n` + ` cargo clippy --all-targets -- -D warnings`,
|
||||
|
||||
@@ -50,11 +50,11 @@ if printf '%s\n' "$staged" | grep -qE '^Server/(db/queries/|migrations/|sqlc\.ya
|
||||
fi
|
||||
|
||||
# Protocol schema changed -> regenerated Go + TS constants must be in the same commit.
|
||||
if printf '%s\n' "$staged" | grep -qE '^(docs/protocol-schema\.json|Server/scripts/genprotocol/)'; then
|
||||
if printf '%s\n' "$staged" | grep -qE '^(protocol/schema\.json|Server/cmd/genprotocol/)'; then
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
(cd Server && go run ./scripts/genprotocol \
|
||||
(cd Server && go run ./cmd/genprotocol \
|
||||
&& git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts) \
|
||||
|| fail "protocol constants are stale — run 'go run ./scripts/genprotocol' in Server/ and stage the result"
|
||||
|| fail "protocol constants are stale — run 'go run ./cmd/genprotocol' in Server/ and stage the result"
|
||||
else
|
||||
printf 'pre-commit: WARNING: go not installed; skipping the protocol-constants check. CI will run it.\n' >&2
|
||||
fi
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ if [ "$changed" = "__all__" ]; then
|
||||
else
|
||||
if printf '%s\n' "$changed" | grep -q '^Server/'; then server_changed=1; fi
|
||||
if printf '%s\n' "$changed" | grep -q '^Client/'; then client_changed=1; fi
|
||||
if printf '%s\n' "$changed" | grep -q '^docs/protocol-schema\.json'; then
|
||||
if printf '%s\n' "$changed" | grep -q '^protocol/schema\.json'; then
|
||||
server_changed=1
|
||||
client_changed=1
|
||||
fi
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
run: make sqlc-install sqlc-verify
|
||||
|
||||
# Protocol message-type constants (Go + TS) must never drift from
|
||||
# docs/protocol-schema.json — the single source of truth.
|
||||
# protocol/schema.json — the single source of truth.
|
||||
- name: Verify generated protocol constants (make protocol-verify)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: make protocol-verify
|
||||
|
||||
Generated
+2
-2
@@ -6306,7 +6306,7 @@ Server/ws/voice_e2ee.go:270-272 (direct publish, bypasses h.broadcast)
|
||||
|
||||
Server/ws/hub_broadcast.go:64-72 documents that publishing straight to pub/sub "would reintroduce exactly that kind of reordering".
|
||||
|
||||
**Suggested fix:** Carry the leaver's join instance in the broadcast and make the client's leave handling instance-conditional. finishVoiceLeave already holds `oldJoinToken` (voice_leave.go:56), so add it to voiceLeavePayload/buildVoiceLeave (via the protocol-change skill, since docs/protocol-schema.json is the source of truth) and record each peer's join token from voice_state in the client. Then guard the top of handleParticipantLeft: if the payload's join token is not the one currently recorded for that peer, ignore the event entirely — one guard in the shared function covers the delete, the retirement and the election at once. A client-only stopgap that removes the permanent half of the damage is to skip the `retirePeerKey` call at :1213-1215 whenever the peer is still present in `voiceStore.voiceUsers.get(channelId)`, which leaves the peer re-announceable instead of permanently blocked.
|
||||
**Suggested fix:** Carry the leaver's join instance in the broadcast and make the client's leave handling instance-conditional. finishVoiceLeave already holds `oldJoinToken` (voice_leave.go:56), so add it to voiceLeavePayload/buildVoiceLeave (via the protocol-change skill, since protocol/schema.json is the source of truth) and record each peer's join token from voice_state in the client. Then guard the top of handleParticipantLeft: if the payload's join token is not the one currently recorded for that peer, ignore the event entirely — one guard in the shared function covers the delete, the retirement and the election at once. A client-only stopgap that removes the permanent half of the damage is to skip the `retirePeerKey` call at :1213-1215 whenever the peer is still present in `voiceStore.voiceUsers.get(channelId)`, which leaves the peer re-announceable instead of permanently blocked.
|
||||
|
||||
**Fixed:** `ccd9f39b69b202dc2858c8b02e97104e67f81aec` · test `Client/tests/unit/livekit-e2ee.test.ts` · revert-proof pass
|
||||
|
||||
@@ -8025,7 +8025,7 @@ The server deliberately withholds a mention badge for an @here from every reader
|
||||
|
||||
**Evidence:** mentions.ts:124-127 `export function highlightsCurrentUser(content, info) { if (info?.mentionsEveryone === true) return true; ... }` — no way to distinguish @here from @everyone. dispatcher.ts:634-649 `const isMention = highlightsCurrentUser(payload.content, {mentions: payload.mentions, mentionsEveryone: payload.mentions_everyone}); ... if (isMention) incrementMention(payload.channel_id, isDetached);`. Server/service/mentions.go:187 `if set.HereOnly && (db.BroadcastStatus(r.Status) == db.StatusOffline || (s.online != nil && !s.online(r.UserID))) { continue }`. Server/ws/messages.go:83 `MentionsEveryone bool \`json:"mentions_everyone"\`` is the only mention-scope field on the wire.
|
||||
|
||||
**Suggested fix:** Stop collapsing the two tokens on the wire, then apply the server's rule once on the client's replay path. (1) Add a `mentions_here` bool to chatMessagePayload/chatEditedPayload (Server/ws/messages.go:83 and :149) sourced from mentionSet.HereOnly (plumb it alongside MentionsEveryone through service/message.go's SendResult/EditResult and ws/handlers_chat.go), regenerating docs/protocol-schema.json -> message_types.go/protocolTypes.ts via the protocol-change skill. (2) In Client/src/lib/dispatcher.ts, hoist the existing `isReplayFrame` computation (currently dispatcher.ts:686-690) above the unread/mention block at 634-649 and gate the badge in that one place: treat a frame as a mention only when `payload.mentions.includes(me)` or (`payload.mentions_everyone && !(payload.mentions_here && isReplayFrame)`). That mirrors applyMentionCounts exactly — a here-only mention delivered in the reconnect burst is by definition one the reader was disconnected for — and leaves live delivery, @everyone, and direct mentions untouched. No change to mentions.ts's highlightsCurrentUser is needed for highlight rendering; only the badge increment must distinguish the two.
|
||||
**Suggested fix:** Stop collapsing the two tokens on the wire, then apply the server's rule once on the client's replay path. (1) Add a `mentions_here` bool to chatMessagePayload/chatEditedPayload (Server/ws/messages.go:83 and :149) sourced from mentionSet.HereOnly (plumb it alongside MentionsEveryone through service/message.go's SendResult/EditResult and ws/handlers_chat.go), regenerating protocol/schema.json -> message_types.go/protocolTypes.ts via the protocol-change skill. (2) In Client/src/lib/dispatcher.ts, hoist the existing `isReplayFrame` computation (currently dispatcher.ts:686-690) above the unread/mention block at 634-649 and gate the badge in that one place: treat a frame as a mention only when `payload.mentions.includes(me)` or (`payload.mentions_everyone && !(payload.mentions_here && isReplayFrame)`). That mirrors applyMentionCounts exactly — a here-only mention delivered in the reconnect burst is by definition one the reader was disconnected for — and leaves live delivery, @everyone, and direct mentions untouched. No change to mentions.ts's highlightsCurrentUser is needed for highlight rendering; only the badge increment must distinguish the two.
|
||||
|
||||
**Fixed:** `6f6d0ae` · test `Client/tests/unit/dispatcher.test.ts` · revert-proof self-reported
|
||||
|
||||
|
||||
@@ -5134,7 +5134,7 @@
|
||||
"test": "Client/tests/unit/livekit-e2ee.test.ts",
|
||||
"revertProof": "pass"
|
||||
},
|
||||
"suggestedFix": "Carry the leaver's join instance in the broadcast and make the client's leave handling instance-conditional. finishVoiceLeave already holds `oldJoinToken` (voice_leave.go:56), so add it to voiceLeavePayload/buildVoiceLeave (via the protocol-change skill, since docs/protocol-schema.json is the source of truth) and record each peer's join token from voice_state in the client. Then guard the top of handleParticipantLeft: if the payload's join token is not the one currently recorded for that peer, ignore the event entirely — one guard in the shared function covers the delete, the retirement and the election at once. A client-only stopgap that removes the permanent half of the damage is to skip the `retirePeerKey` call at :1213-1215 whenever the peer is still present in `voiceStore.voiceUsers.get(channelId)`, which leaves the peer re-announceable instead of permanently blocked.",
|
||||
"suggestedFix": "Carry the leaver's join instance in the broadcast and make the client's leave handling instance-conditional. finishVoiceLeave already holds `oldJoinToken` (voice_leave.go:56), so add it to voiceLeavePayload/buildVoiceLeave (via the protocol-change skill, since protocol/schema.json is the source of truth) and record each peer's join token from voice_state in the client. Then guard the top of handleParticipantLeft: if the payload's join token is not the one currently recorded for that peer, ignore the event entirely — one guard in the shared function covers the delete, the retirement and the election at once. A client-only stopgap that removes the permanent half of the damage is to skip the `retirePeerKey` call at :1213-1215 whenever the peer is still present in `voiceStore.voiceUsers.get(channelId)`, which leaves the peer re-announceable instead of permanently blocked.",
|
||||
"fixedDate": "2026-08-20"
|
||||
},
|
||||
{
|
||||
@@ -6426,7 +6426,7 @@
|
||||
"lens": "flow-message",
|
||||
"finder": "opus",
|
||||
"confidence": "medium",
|
||||
"suggestedFix": "Stop collapsing the two tokens on the wire, then apply the server's rule once on the client's replay path. (1) Add a `mentions_here` bool to chatMessagePayload/chatEditedPayload (Server/ws/messages.go:83 and :149) sourced from mentionSet.HereOnly (plumb it alongside MentionsEveryone through service/message.go's SendResult/EditResult and ws/handlers_chat.go), regenerating docs/protocol-schema.json -> message_types.go/protocolTypes.ts via the protocol-change skill. (2) In Client/src/lib/dispatcher.ts, hoist the existing `isReplayFrame` computation (currently dispatcher.ts:686-690) above the unread/mention block at 634-649 and gate the badge in that one place: treat a frame as a mention only when `payload.mentions.includes(me)` or (`payload.mentions_everyone && !(payload.mentions_here && isReplayFrame)`). That mirrors applyMentionCounts exactly — a here-only mention delivered in the reconnect burst is by definition one the reader was disconnected for — and leaves live delivery, @everyone, and direct mentions untouched. No change to mentions.ts's highlightsCurrentUser is needed for highlight rendering; only the badge increment must distinguish the two.",
|
||||
"suggestedFix": "Stop collapsing the two tokens on the wire, then apply the server's rule once on the client's replay path. (1) Add a `mentions_here` bool to chatMessagePayload/chatEditedPayload (Server/ws/messages.go:83 and :149) sourced from mentionSet.HereOnly (plumb it alongside MentionsEveryone through service/message.go's SendResult/EditResult and ws/handlers_chat.go), regenerating protocol/schema.json -> message_types.go/protocolTypes.ts via the protocol-change skill. (2) In Client/src/lib/dispatcher.ts, hoist the existing `isReplayFrame` computation (currently dispatcher.ts:686-690) above the unread/mention block at 634-649 and gate the badge in that one place: treat a frame as a mention only when `payload.mentions.includes(me)` or (`payload.mentions_everyone && !(payload.mentions_here && isReplayFrame)`). That mirrors applyMentionCounts exactly — a here-only mention delivered in the reconnect burst is by definition one the reader was disconnected for — and leaves live delivery, @everyone, and direct mentions untouched. No change to mentions.ts's highlightsCurrentUser is needed for highlight rendering; only the badge increment must distinguish the two.",
|
||||
"fix": {
|
||||
"commit": "6f6d0ae",
|
||||
"test": "Client/tests/unit/dispatcher.test.ts",
|
||||
|
||||
@@ -14,7 +14,7 @@ CI fails on drift, and the next generator run silently discards your edit.
|
||||
| Generated | Source of truth | Workflow |
|
||||
| ---------------------------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `Server/db/dbgen/` | `Server/db/queries/*.sql`, `Server/migrations/` | `db-change` skill |
|
||||
| `Server/ws/message_types.go` **and** `Client/src/lib/protocolTypes.ts` | `docs/protocol-schema.json` | `protocol-change` skill |
|
||||
| `Server/ws/message_types.go` **and** `Client/src/lib/protocolTypes.ts` | `protocol/schema.json` | `protocol-change` skill |
|
||||
| `Client/src/generated/` | `tauri-typegen` | CI patches known typegen bugs — see `.github/workflows/ci.yml` |
|
||||
|
||||
## Bug-hunt ledger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by scripts/genprotocol from docs/protocol-schema.json; DO NOT EDIT.
|
||||
// Code generated by cmd/genprotocol from protocol/schema.json; DO NOT EDIT.
|
||||
//
|
||||
// Shared WebSocket protocol message type constants — single source of truth
|
||||
// for both Server (Go) and Client (TypeScript). Edit docs/protocol-schema.json
|
||||
// for both Server (Go) and Client (TypeScript). Edit protocol/schema.json
|
||||
// and run `make protocol-generate` in Server/.
|
||||
//
|
||||
// Usage: import { MessageType } from "@lib/protocolTypes";
|
||||
|
||||
@@ -346,7 +346,7 @@ export function chatEchoHandlers(): Array<{ type: string; handler: string }> {
|
||||
* Voice WS flow handlers for E2E testing.
|
||||
* Simulates the server-side voice protocol defined in:
|
||||
* docs/brain/06-Specs/PROTOCOL.md (voice_join, voice_leave, voice_token, voice_token_refresh)
|
||||
* docs/protocol-schema.json (message type schemas)
|
||||
* protocol/schema.json (message type schemas)
|
||||
*
|
||||
* When PROTOCOL.md voice message types change, update these handlers to match.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ tmp_dir = "tmp"
|
||||
bin = "./chatserver.exe"
|
||||
cmd = "go build -o chatserver.exe -ldflags \"-s -w\" ."
|
||||
delay = 1000
|
||||
exclude_dir = ["tmp", "scripts", "migrations", "data", "admin/static"]
|
||||
exclude_dir = ["tmp", "scripts", "cmd", "migrations", "data", "admin/static"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test\\.go$"]
|
||||
exclude_unchanged = false
|
||||
|
||||
@@ -13,8 +13,9 @@ data/
|
||||
# Local config (users provide their own via volume mount or env)
|
||||
config.yaml
|
||||
|
||||
# Scripts (not needed in image)
|
||||
# Developer tooling (not needed in image; the image builds the root package)
|
||||
scripts/
|
||||
cmd/
|
||||
|
||||
# Git metadata
|
||||
.git
|
||||
|
||||
@@ -72,7 +72,7 @@ linters:
|
||||
excludes:
|
||||
- G104 # unhandled errors — errcheck covers this better
|
||||
- G304 # file path from variable — expected in file storage code
|
||||
- G306 # WriteFile perms ≤0600 — our only hits are generated source files (genprotocol), which must stay world-readable or multi-stage container builds break
|
||||
- G306 # WriteFile perms ≤0600 — our only hits are generated source files (cmd/genprotocol), which must stay world-readable or multi-stage container builds break
|
||||
- G706 # log injection — false positive with slog structured logging (values are typed key-value pairs, not interpolated)
|
||||
|
||||
exclusions:
|
||||
|
||||
@@ -9,6 +9,8 @@ prometheus.
|
||||
- `api/` REST handlers · `ws/` WebSocket hub · `auth/` sessions/TOTP ·
|
||||
`permissions/` role checks · `service/` domain logic shared by both entry points
|
||||
- `db/` hand-written query wrappers; `db/dbgen/` is generated (see `db-change`)
|
||||
- `cmd/` executable tooling, one `package main` per subdirectory —
|
||||
`cmd/genprotocol/` regenerates the protocol constants from `protocol/schema.json`
|
||||
- `admin/` web admin panel · `updater/` self-update + signature verification ·
|
||||
`plugin/` WASM plugin runtime (`-tags wazero`) · `telemetry/` OTel (`-tags otel`)
|
||||
- `syncutil/` lock helpers that gain deadlock detection under `-tags deadlock`
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@
|
||||
# sqlc-generate Regenerate type-safe Go from sqlc.yaml (db/dbgen).
|
||||
# sqlc-verify Fail if the committed dbgen output is stale (used by CI).
|
||||
# sqlc-install Install the pinned sqlc version into $GOBIN.
|
||||
# protocol-generate Regenerate WS message-type constants (Go + TS) from docs/protocol-schema.json.
|
||||
# protocol-generate Regenerate WS message-type constants (Go + TS) from ../protocol/schema.json.
|
||||
# protocol-verify Fail if the committed protocol constants are stale (used by CI).
|
||||
# otel-up Start Jaeger + Prometheus for local tracing development.
|
||||
# otel-down Stop and remove the OTel dev containers.
|
||||
@@ -82,10 +82,10 @@ sqlc-verify:
|
||||
)
|
||||
|
||||
protocol-generate:
|
||||
go run ./scripts/genprotocol
|
||||
go run ./cmd/genprotocol
|
||||
|
||||
protocol-verify:
|
||||
go run ./scripts/genprotocol
|
||||
go run ./cmd/genprotocol
|
||||
@git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts || ( \
|
||||
echo "ERROR: generated protocol constants are stale. Run 'make protocol-generate' and commit the result." ; \
|
||||
exit 1 ; \
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// genprotocol generates the WebSocket protocol message-type constant files
|
||||
// for both the Go server and the TypeScript client from the single source of
|
||||
// truth at docs/protocol-schema.json.
|
||||
// truth at protocol/schema.json.
|
||||
//
|
||||
// Usage (from the Server/ directory):
|
||||
//
|
||||
// go run ./scripts/genprotocol
|
||||
// go run ./cmd/genprotocol
|
||||
//
|
||||
// or via make:
|
||||
//
|
||||
@@ -38,7 +38,7 @@ type schema struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
schemaPath := flag.String("schema", "../docs/protocol-schema.json", "path to protocol-schema.json")
|
||||
schemaPath := flag.String("schema", "../protocol/schema.json", "path to the protocol schema")
|
||||
goOut := flag.String("go-out", "ws/message_types.go", "path to the generated Go file")
|
||||
tsOut := flag.String("ts-out", "../Client/src/lib/protocolTypes.ts", "path to the generated TypeScript file")
|
||||
flag.Parse()
|
||||
@@ -95,14 +95,14 @@ func validate(s schema) error {
|
||||
}
|
||||
|
||||
func header(comment string) string {
|
||||
return "// Code generated by scripts/genprotocol from docs/protocol-schema.json; DO NOT EDIT.\n" +
|
||||
return "// Code generated by cmd/genprotocol from protocol/schema.json; DO NOT EDIT.\n" +
|
||||
comment + "\n"
|
||||
}
|
||||
|
||||
func renderGo(s schema) ([]byte, error) {
|
||||
var b strings.Builder
|
||||
b.WriteString(header("//\n// WebSocket protocol message type constants — single source of truth for\n" +
|
||||
"// both Server (Go) and Client (TypeScript). Edit docs/protocol-schema.json\n" +
|
||||
"// both Server (Go) and Client (TypeScript). Edit protocol/schema.json\n" +
|
||||
"// and run `make protocol-generate` (see Server/Makefile)."))
|
||||
b.WriteString("\npackage ws\n\n")
|
||||
|
||||
@@ -131,7 +131,7 @@ func renderGo(s schema) ([]byte, error) {
|
||||
func renderTS(s schema) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(header("//\n// Shared WebSocket protocol message type constants — single source of truth\n" +
|
||||
"// for both Server (Go) and Client (TypeScript). Edit docs/protocol-schema.json\n" +
|
||||
"// for both Server (Go) and Client (TypeScript). Edit protocol/schema.json\n" +
|
||||
"// and run `make protocol-generate` in Server/.\n//\n" +
|
||||
"// Usage: import { MessageType } from \"@lib/protocolTypes\";\n" +
|
||||
"// ws.send({ type: MessageType.CHAT_SEND, payload: { ... } });"))
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// The wire protocol is the envelope format from docs/protocol.md: every
|
||||
// client->server frame is {type, id?, payload:{...}} and the first frame MUST
|
||||
// be an `auth` envelope. If you change docs/protocol-schema.json, grep this
|
||||
// be an `auth` envelope. If you change protocol/schema.json, grep this
|
||||
// script — it is not generated and CI does not run it, so it rots silently
|
||||
// (it once drifted to pre-envelope framing and reported green while every
|
||||
// auth failed).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by scripts/genprotocol from docs/protocol-schema.json; DO NOT EDIT.
|
||||
// Code generated by cmd/genprotocol from protocol/schema.json; DO NOT EDIT.
|
||||
//
|
||||
// WebSocket protocol message type constants — single source of truth for
|
||||
// both Server (Go) and Client (TypeScript). Edit docs/protocol-schema.json
|
||||
// both Server (Go) and Client (TypeScript). Edit protocol/schema.json
|
||||
// and run `make protocol-generate` (see Server/Makefile).
|
||||
|
||||
package ws
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package ws_test
|
||||
|
||||
// protocol_contract_test.go — locks docs/protocol-schema.json and the
|
||||
// protocol_contract_test.go — locks protocol/schema.json and the
|
||||
// generated ws/message_types.go together so the two cannot silently drift.
|
||||
//
|
||||
// message_types.go is documented as "Code generated by scripts/genprotocol
|
||||
// from docs/protocol-schema.json; DO NOT EDIT" and CI runs
|
||||
// message_types.go is documented as "Code generated by cmd/genprotocol
|
||||
// from protocol/schema.json; DO NOT EDIT" and CI runs
|
||||
// `make protocol-verify`, but that only re-runs the generator and diffs its
|
||||
// output — it says nothing about message-type constants that exist in the ws
|
||||
// package outside the generated file (e.g. a handler defining its own
|
||||
@@ -41,7 +41,7 @@ import (
|
||||
var knownUndocumentedConstants = map[string]string{}
|
||||
|
||||
// protocolSchemaEntry mirrors one element of the client_to_server /
|
||||
// server_to_client arrays in docs/protocol-schema.json.
|
||||
// server_to_client arrays in protocol/schema.json.
|
||||
type protocolSchemaEntry struct {
|
||||
Wire string `json:"wire"`
|
||||
Go string `json:"go"`
|
||||
@@ -54,8 +54,8 @@ type protocolSchema struct {
|
||||
ServerToClient []protocolSchemaEntry `json:"server_to_client"`
|
||||
}
|
||||
|
||||
// loadProtocolSchema locates and parses docs/protocol-schema.json relative to
|
||||
// this test file (ws/ -> Server/ -> repo root -> docs/), so the test does not
|
||||
// loadProtocolSchema locates and parses protocol/schema.json relative to
|
||||
// this test file (ws/ -> Server/ -> repo root -> protocol/), so the test does not
|
||||
// depend on the working directory `go test` happens to be invoked from.
|
||||
func loadProtocolSchema(t *testing.T) protocolSchema {
|
||||
t.Helper()
|
||||
@@ -64,7 +64,7 @@ func loadProtocolSchema(t *testing.T) protocolSchema {
|
||||
t.Fatal("runtime.Caller failed to resolve test file path")
|
||||
}
|
||||
wsDir := filepath.Dir(thisFile)
|
||||
schemaPath := filepath.Join(wsDir, "..", "..", "docs", "protocol-schema.json")
|
||||
schemaPath := filepath.Join(wsDir, "..", "..", "protocol", "schema.json")
|
||||
|
||||
raw, err := os.ReadFile(schemaPath)
|
||||
if err != nil {
|
||||
@@ -145,7 +145,7 @@ func loadGoMsgTypeConstants(t *testing.T) map[string]string {
|
||||
}
|
||||
|
||||
// TestProtocolSchema_MatchesGeneratedGoConstants is the "schema -> code"
|
||||
// direction: every wire constant docs/protocol-schema.json lists (in either
|
||||
// direction: every wire constant protocol/schema.json lists (in either
|
||||
// direction of traffic) must have a same-named Go constant in the ws package
|
||||
// carrying exactly the schema's wire string.
|
||||
func TestProtocolSchema_MatchesGeneratedGoConstants(t *testing.T) {
|
||||
@@ -161,16 +161,16 @@ func TestProtocolSchema_MatchesGeneratedGoConstants(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
if got != e.Wire {
|
||||
t.Errorf("%s: ws.%s = %q, want %q per protocol-schema.json", direction, e.Go, got, e.Wire)
|
||||
t.Errorf("%s: ws.%s = %q, want %q per protocol/schema.json", direction, e.Go, got, e.Wire)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(schema.ClientToServer) == 0 {
|
||||
t.Fatal("protocol-schema.json client_to_server is empty — schema failed to load")
|
||||
t.Fatal("protocol/schema.json client_to_server is empty — schema failed to load")
|
||||
}
|
||||
if len(schema.ServerToClient) == 0 {
|
||||
t.Fatal("protocol-schema.json server_to_client is empty — schema failed to load")
|
||||
t.Fatal("protocol/schema.json server_to_client is empty — schema failed to load")
|
||||
}
|
||||
|
||||
check("client_to_server", schema.ClientToServer)
|
||||
@@ -179,7 +179,7 @@ func TestProtocolSchema_MatchesGeneratedGoConstants(t *testing.T) {
|
||||
|
||||
// TestProtocolSchema_NoUndocumentedGoConstants is the "code -> schema"
|
||||
// direction: every MsgType* constant declared in the ws package must appear
|
||||
// in docs/protocol-schema.json, except the documented exceptions in
|
||||
// in protocol/schema.json, except the documented exceptions in
|
||||
// knownUndocumentedConstants (see its doc comment). This is what catches a
|
||||
// handler minting its own wire constant instead of adding it to the schema.
|
||||
func TestProtocolSchema_NoUndocumentedGoConstants(t *testing.T) {
|
||||
@@ -200,7 +200,7 @@ func TestProtocolSchema_NoUndocumentedGoConstants(t *testing.T) {
|
||||
}
|
||||
exceptWire, isException := knownUndocumentedConstants[name]
|
||||
if !isException {
|
||||
t.Errorf("ws.%s = %q is not in docs/protocol-schema.json and is not a documented exception "+
|
||||
t.Errorf("ws.%s = %q is not in protocol/schema.json and is not a documented exception "+
|
||||
"(knownUndocumentedConstants) — add it to the schema or the exception list", name, wire)
|
||||
continue
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func TestProtocolSchema_NoUndocumentedGoConstants(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
if _, nowDocumented := documented[name]; nowDocumented {
|
||||
t.Errorf("knownUndocumentedConstants lists %q but it is now in protocol-schema.json — remove it from the exception list", name)
|
||||
t.Errorf("knownUndocumentedConstants lists %q but it is now in protocol/schema.json — remove it from the exception list", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -37,14 +37,14 @@ dated snapshots that were true when written and were never updated, and
|
||||
These describe contracts the code implements. If one disagrees with the code,
|
||||
the code is right and the document is a bug.
|
||||
|
||||
| Document | Covers |
|
||||
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [api.md](api.md) | REST API under `/api/v1`. |
|
||||
| [protocol.md](protocol.md) | WebSocket protocol — frames, sequencing, reconnect. |
|
||||
| [schema.md](schema.md) | SQLite schema and migrations. |
|
||||
| [server-configuration.md](server-configuration.md) | Every server configuration option. |
|
||||
| [credential-storage.md](credential-storage.md) | What the desktop client persists, and where. |
|
||||
| [protocol-schema.json](protocol-schema.json) | **Generated-code source of truth.** `Server/ws/message_types.go` and `Client/src/lib/protocolTypes.ts` are generated from it — never hand-edit either. |
|
||||
| Document | Covers |
|
||||
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [api.md](api.md) | REST API under `/api/v1`. |
|
||||
| [protocol.md](protocol.md) | WebSocket protocol — frames, sequencing, reconnect. |
|
||||
| [schema.md](schema.md) | SQLite schema and migrations. |
|
||||
| [server-configuration.md](server-configuration.md) | Every server configuration option. |
|
||||
| [credential-storage.md](credential-storage.md) | What the desktop client persists, and where. |
|
||||
| [../protocol/schema.json](../protocol/schema.json) | **Generated-code source of truth**, at the repository root because it is owned by neither side. `Server/ws/message_types.go` and `Client/src/lib/protocolTypes.ts` are generated from it — never hand-edit either. See [../protocol/README.md](../protocol/README.md). |
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ implements the real-time engine: a single `Hub` owning all client connections,
|
||||
a topic-based pub/sub, a monotonic sequence counter, a 3-tier reconnect replay
|
||||
pipeline, and a single typed (V2) command dispatch.
|
||||
|
||||
Message-type constants are **generated**: `docs/protocol-schema.json` is the
|
||||
single source of truth, and `Server/scripts/genprotocol` emits both
|
||||
Message-type constants are **generated**: `protocol/schema.json` is the
|
||||
single source of truth, and `Server/cmd/genprotocol` emits both
|
||||
`Server/ws/message_types.go` and
|
||||
`Client/src/lib/protocolTypes.ts` from it
|
||||
(`make protocol-generate`; CI fails on drift via `make protocol-verify`).
|
||||
|
||||
+13
-13
@@ -60,19 +60,19 @@ next section, and using them directly is equally correct.
|
||||
|
||||
**Make targets** (run from `Server/`):
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `make test` | Run the test suite the way CI does (`-race`, 20 min timeout) |
|
||||
| `make test-deadlock` | Run the deadlock-detection pass CI also runs (`-tags deadlock`) |
|
||||
| `make cover` | Per-package coverage (what CI uploads) + a function summary |
|
||||
| `make cover-all` | Cross-package coverage — the honest number (also lists 0.0% functions) |
|
||||
| `make sqlc-install` | Install the pinned sqlc version into `$GOBIN` |
|
||||
| `make sqlc-generate` | Regenerate the type-safe Go query layer (`db/dbgen/`, SQLite engine) |
|
||||
| `make sqlc-verify` | Fail if the committed `dbgen` output is stale (used by CI) |
|
||||
| `make protocol-generate` | Regenerate the WS message-type constants (Go + TS) from `docs/protocol-schema.json` |
|
||||
| `make protocol-verify` | Fail if the committed protocol constants are stale (used by CI) |
|
||||
| `make otel-up` | Start Jaeger (traces) + Prometheus (metrics) via Docker for local OTel development |
|
||||
| `make otel-down` | Stop and remove the OTel dev containers |
|
||||
| Command | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| `make test` | Run the test suite the way CI does (`-race`, 20 min timeout) |
|
||||
| `make test-deadlock` | Run the deadlock-detection pass CI also runs (`-tags deadlock`) |
|
||||
| `make cover` | Per-package coverage (what CI uploads) + a function summary |
|
||||
| `make cover-all` | Cross-package coverage — the honest number (also lists 0.0% functions) |
|
||||
| `make sqlc-install` | Install the pinned sqlc version into `$GOBIN` |
|
||||
| `make sqlc-generate` | Regenerate the type-safe Go query layer (`db/dbgen/`, SQLite engine) |
|
||||
| `make sqlc-verify` | Fail if the committed `dbgen` output is stale (used by CI) |
|
||||
| `make protocol-generate` | Regenerate the WS message-type constants (Go + TS) from `protocol/schema.json` |
|
||||
| `make protocol-verify` | Fail if the committed protocol constants are stale (used by CI) |
|
||||
| `make otel-up` | Start Jaeger (traces) + Prometheus (metrics) via Docker for local OTel development |
|
||||
| `make otel-down` | Stop and remove the OTel dev containers |
|
||||
|
||||
#### Client (Tauri v2)
|
||||
|
||||
|
||||
+2
-2
@@ -1494,7 +1494,7 @@ recipient from being flooded.
|
||||
|
||||
## Message Type Reference Table
|
||||
|
||||
The authoritative type inventory is [protocol-schema.json](protocol-schema.json),
|
||||
The authoritative type inventory is [protocol/schema.json](../protocol/schema.json),
|
||||
from which the Go and TypeScript constant files are generated
|
||||
(`make protocol-generate` / verified in CI by `make protocol-verify`). The
|
||||
tables below add per-type behavioral notes.
|
||||
@@ -1578,7 +1578,7 @@ tables below add per-type behavioral notes.
|
||||
### Plugin command types
|
||||
|
||||
Three wire types exist for the WASM plugin system. Since 2026-08-04 they are
|
||||
listed in `protocol-schema.json` like every other type (closing DC-01), so
|
||||
listed in `protocol/schema.json` like every other type (closing DC-01), so
|
||||
the generated constants cover them and `make protocol-verify` plus the
|
||||
`ws` package's protocol-contract test gate them against drift.
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# `protocol/`
|
||||
|
||||
The cross-component WebSocket contract. It lives at the repository root
|
||||
because neither side owns it: `schema.json` is the single source of truth for
|
||||
the message-type constants **both** the Go server and the TypeScript client
|
||||
compile against.
|
||||
|
||||
| File | Role |
|
||||
| ------------- | --------------------------------------------------------- |
|
||||
| `schema.json` | Source of truth. Every wire message type, both directions |
|
||||
|
||||
Two files are generated from it and must never be hand-edited:
|
||||
|
||||
- `Server/ws/message_types.go`
|
||||
- `Client/src/lib/protocolTypes.ts`
|
||||
|
||||
## Changing the protocol
|
||||
|
||||
Edit `schema.json`, then regenerate both consumers with one command from the
|
||||
repository root:
|
||||
|
||||
```bash
|
||||
npm run generate
|
||||
```
|
||||
|
||||
(Equivalently, `make protocol-generate` or `go run ./cmd/genprotocol` from
|
||||
`Server/` — the generator is a Go program, so it lives where the Go toolchain
|
||||
already runs.)
|
||||
|
||||
Three gates reject a stale regeneration and one gate checks the schema against
|
||||
the constants independently — `.githooks/pre-commit`, `make protocol-verify` in
|
||||
CI, `npm run check:server`, and `Server/ws/protocol_contract_test.go`. There is
|
||||
nothing extra to run.
|
||||
|
||||
The narrative protocol reference is [`docs/protocol.md`](../docs/protocol.md);
|
||||
the blueprint is [`docs/architecture/websocket.md`](../docs/architecture/websocket.md).
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$comment": "Single source of truth for WebSocket protocol message-type constants. Server/ws/message_types.go and Client/src/lib/protocolTypes.ts are generated from this file — edit here, then run `make protocol-generate` in Server/. CI runs `make protocol-verify` to reject drift.",
|
||||
"$comment": "Single source of truth for WebSocket protocol message-type constants. Server/ws/message_types.go and Client/src/lib/protocolTypes.ts are generated from this file — edit here, then run `npm run generate` from the repository root (or `make protocol-generate` in Server/). CI runs `make protocol-verify` to reject drift.",
|
||||
"version": 1,
|
||||
"client_to_server": [
|
||||
{ "wire": "auth", "go": "MsgTypeAuth", "ts": "AUTH" },
|
||||
+2
-2
@@ -59,7 +59,7 @@ const tracked = (...patterns) => {
|
||||
// `git diff --exit-code` after regenerating is what `make protocol-verify` and
|
||||
// `make sqlc-verify` reduce to. Inlined so neither needs make.
|
||||
const PROTOCOL_VERIFY = [
|
||||
step("go", ["run", "./scripts/genprotocol"], "Server"),
|
||||
step("go", ["run", "./cmd/genprotocol"], "Server"),
|
||||
step(
|
||||
"git",
|
||||
["diff", "--exit-code", "ws/message_types.go", "../Client/src/lib/protocolTypes.ts"],
|
||||
@@ -151,7 +151,7 @@ const TASKS = {
|
||||
"check:hygiene": CHECK_HYGIENE,
|
||||
check: [...CHECK_DOCS, ...CHECK_HYGIENE, ...CHECK_SERVER, ...CHECK_CLIENT, ...CHECK_RUST],
|
||||
generate: [
|
||||
step("go", ["run", "./scripts/genprotocol"], "Server"),
|
||||
step("go", ["run", "./cmd/genprotocol"], "Server"),
|
||||
optional(
|
||||
"sqlc",
|
||||
"sqlc",
|
||||
|
||||
Reference in New Issue
Block a user