mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
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
165 lines
5.5 KiB
Go
165 lines
5.5 KiB
Go
// 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 protocol/schema.json.
|
|
//
|
|
// Usage (from the Server/ directory):
|
|
//
|
|
// go run ./cmd/genprotocol
|
|
//
|
|
// or via make:
|
|
//
|
|
// make protocol-generate # regenerate both outputs
|
|
// make protocol-verify # fail if committed outputs are stale (CI)
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"go/format"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
type message struct {
|
|
Wire string `json:"wire"`
|
|
Go string `json:"go"`
|
|
TS string `json:"ts"`
|
|
Note string `json:"note,omitempty"`
|
|
GoTrailingComment string `json:"go_trailing_comment,omitempty"`
|
|
}
|
|
|
|
type schema struct {
|
|
Comment string `json:"$comment"`
|
|
Version int `json:"version"`
|
|
ClientToServer []message `json:"client_to_server"`
|
|
ServerToClient []message `json:"server_to_client"`
|
|
}
|
|
|
|
func main() {
|
|
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()
|
|
|
|
raw, err := os.ReadFile(*schemaPath)
|
|
if err != nil {
|
|
log.Fatalf("read schema: %v", err)
|
|
}
|
|
var s schema
|
|
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&s); err != nil {
|
|
log.Fatalf("parse schema: %v", err)
|
|
}
|
|
if err := validate(s); err != nil {
|
|
log.Fatalf("invalid schema: %v", err)
|
|
}
|
|
|
|
goSrc, err := renderGo(s)
|
|
if err != nil {
|
|
log.Fatalf("render Go: %v", err)
|
|
}
|
|
if err := os.WriteFile(*goOut, goSrc, 0o644); err != nil {
|
|
log.Fatalf("write %s: %v", *goOut, err)
|
|
}
|
|
if err := os.WriteFile(*tsOut, []byte(renderTS(s)), 0o644); err != nil {
|
|
log.Fatalf("write %s: %v", *tsOut, err)
|
|
}
|
|
fmt.Printf("generated %s (%d client→server, %d server→client) and %s\n",
|
|
*goOut, len(s.ClientToServer), len(s.ServerToClient), *tsOut)
|
|
}
|
|
|
|
// validate rejects duplicate identifiers and empty fields early so a bad
|
|
// schema edit fails the generator instead of producing broken output.
|
|
func validate(s schema) error {
|
|
goNames := map[string]bool{}
|
|
for _, list := range [][]message{s.ClientToServer, s.ServerToClient} {
|
|
tsNames := map[string]bool{}
|
|
for _, m := range list {
|
|
if m.Wire == "" || m.Go == "" || m.TS == "" {
|
|
return fmt.Errorf("entry %+v: wire, go, and ts are all required", m)
|
|
}
|
|
if goNames[m.Go] {
|
|
return fmt.Errorf("duplicate Go constant %q", m.Go)
|
|
}
|
|
goNames[m.Go] = true
|
|
if tsNames[m.TS] {
|
|
return fmt.Errorf("duplicate TS key %q within one direction", m.TS)
|
|
}
|
|
tsNames[m.TS] = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func header(comment string) string {
|
|
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 protocol/schema.json\n" +
|
|
"// and run `make protocol-generate` (see Server/Makefile)."))
|
|
b.WriteString("\npackage ws\n\n")
|
|
|
|
writeBlock := func(title string, msgs []message) {
|
|
b.WriteString("// " + title + "\nconst (\n")
|
|
for _, m := range msgs {
|
|
b.WriteString("\t" + m.Go + " = " + fmt.Sprintf("%q", m.Wire))
|
|
switch {
|
|
case m.GoTrailingComment != "":
|
|
b.WriteString(" " + m.GoTrailingComment)
|
|
case m.Note != "":
|
|
b.WriteString(" // " + m.Note)
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString(")\n")
|
|
}
|
|
|
|
writeBlock("Client → Server message types (received by handlers).", s.ClientToServer)
|
|
b.WriteString("\n")
|
|
writeBlock("Server → Client message types (sent in broadcasts/responses).", s.ServerToClient)
|
|
|
|
return format.Source([]byte(b.String()))
|
|
}
|
|
|
|
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 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: { ... } });"))
|
|
|
|
writeBlock := func(title, name string, msgs []message) {
|
|
b.WriteString("\n// " + strings.Repeat("-", 75) + "\n")
|
|
b.WriteString("// " + title + "\n")
|
|
b.WriteString("// " + strings.Repeat("-", 75) + "\n\n")
|
|
b.WriteString("export const " + name + " = {\n")
|
|
for _, m := range msgs {
|
|
b.WriteString(" " + m.TS + ": " + fmt.Sprintf("%q", m.Wire) + ",")
|
|
if m.Note != "" {
|
|
b.WriteString(" // " + m.Note)
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("} as const;\n\n")
|
|
b.WriteString("export type " + name + "Value = (typeof " + name + ")[keyof typeof " + name + "];\n")
|
|
}
|
|
|
|
writeBlock("Server → Client message types", "ServerMessageType", s.ServerToClient)
|
|
writeBlock("Client → Server message types", "ClientMessageType", s.ClientToServer)
|
|
|
|
b.WriteString("\n// " + strings.Repeat("-", 75) + "\n")
|
|
b.WriteString("// Unified MessageType — all message types in one object for convenience\n")
|
|
b.WriteString("// " + strings.Repeat("-", 75) + "\n\n")
|
|
b.WriteString("export const MessageType = {\n ...ServerMessageType,\n ...ClientMessageType,\n} as const;\n\n")
|
|
b.WriteString("export type MessageTypeValue = (typeof MessageType)[keyof typeof MessageType];\n")
|
|
return b.String()
|
|
}
|