feat: implement new communication patterns and add cross-test support

- Add Transport class for unified communication layer
- Implement new communicator patterns
- Add VERSIONING.md for version management
- Add crosstest files for Dart/Go integration testing
- Update protocol documentation
- Add new skills and templates for AI-assisted development
- Various bug fixes and improvements to Dart implementation
This commit is contained in:
toki 2026-04-12 07:53:31 +09:00
parent 0703373a30
commit c384516b47
19 changed files with 949 additions and 32 deletions

View file

@ -5,7 +5,8 @@
"Bash(apt-get install:*)",
"Bash(sudo apt-get:*)",
"Bash(protoc --version)",
"Bash(dart analyze:*)"
"Bash(dart analyze:*)",
"Bash(ls *.log)"
],
"additionalDirectories": [
"/config/workspace/toki_socket/tasks"

View file

@ -2,8 +2,8 @@
## 공통 원칙
- **레퍼런스 구현체: `go/`**
- Dart 구현체는 레거시 패턴이 있으므로 참고하지 않는
- **레퍼런스 구현체: `go/`, `dart/`**
- 동시성·close-once 구조는 Go 구현체를 우선 참고하고, Dart 구현체는 public API와 parser map 사용 예시를 함께 참고한
- 프로토콜 정의는 `PROTOCOL.md`가 단일 기준이다
- 각 언어의 관용적 패턴을 따르되, 아래 핵심 구조는 반드시 유지한다
@ -20,6 +20,19 @@
---
## 새 언어 추가 절차
1. 새 언어 디렉터리를 만들기 전에 `templates/language/`의 문서를 복사해 패키지 README와 체크리스트 초안으로 사용한다.
2. `PROTOCOL.md``VERSIONING.md`의 현재 protocol version을 기준으로 지원 범위를 정한다.
3. canonical proto인 `dart/lib/src/packets/message_common.proto`에서 언어별 protobuf binding을 생성한다. Go처럼 generator 전용 option이 필요하면 언어 패키지 안에 copy를 둘 수 있지만, message schema는 `tools/check_proto_sync.sh`로 검증 가능해야 한다.
4. `Transport``Communicator`를 먼저 구현하고, 그 위에 TCP/TLS+TCP/WS/WSS client/server를 올린다.
5. 같은 언어 단위 테스트를 작성한 뒤 `skills/add-crosstest-language/SKILL.md`의 runner 배치 규칙에 맞춰 양방향 crosstest를 추가한다.
6. README의 Implementations 표를 `Available`로 바꾸기 전에 formatter/linter, 단위 테스트, cross-language tests를 모두 통과시킨다.
템플릿은 시작점과 완료 조건만 고정한다. 실제 소스 구조, 패키지 매니저, async runtime은 각 언어의 관용적 선택을 따른다.
---
## C# (Unity / .NET)
### 핵심 매핑

View file

@ -3,6 +3,8 @@
Binary TCP and WebSocket protocol using Protocol Buffers for message framing and type-based routing.
Designed for bidirectional, heterogeneous communication across languages and platforms.
Current protocol version: `0.1`
---
## Scope
@ -31,7 +33,7 @@ This protocol intentionally defines only the transport-level contract shared acr
- 4 bytes, big-endian positive integer. Dart reads/writes it as `int32`; Go reads/writes the same 4 bytes as `uint32`.
- Value: byte length of the following `PacketBase` protobuf payload
- Value of `0`: reserved / no-op, receiver clears buffer
- Implementations may apply safety limits to payload size. The current Go implementation rejects TCP packets larger than 64 MiB.
- Implementations may apply safety limits to payload size. The current Dart and Go implementations reject TCP packets larger than 64 MiB.
### WebSocket / WSS
@ -95,6 +97,8 @@ Side A Side B
- If no response within `heartbeatWaitTime` seconds → `onDisconnected()``dispose()`
- A side that is already waiting for a heartbeat response consumes the echo and does not send another heartbeat. A side that is not waiting replies with `HeartBeat`.
Changing heartbeat timing semantics, response behavior, or disconnect rules is a breaking protocol candidate. Document the compatibility plan in [VERSIONING.md](VERSIONING.md) before changing it.
---
## typeName Convention
@ -116,6 +120,8 @@ The current packet proto has no `package` declaration, so Dart and Go both use s
**Important**: Verify typeName consistency across languages before connecting heterogeneous clients.
Changing the `typeName` derivation rule is a breaking protocol change unless all released implementations keep a compatibility path. See [VERSIONING.md](VERSIONING.md).
---
## nonce
@ -124,6 +130,8 @@ The current packet proto has no `package` declaration, so Dart and Go both use s
- Increments by `1` on every send call (including requests and responses)
- Used for request-response correlation via `responseNonce`
Changing `nonce` or `responseNonce` semantics is a breaking protocol change unless older peers can continue to correlate requests and responses correctly.
---
## Request-Response Pattern

View file

@ -26,6 +26,8 @@ See [PROTOCOL.md](PROTOCOL.md) for the full wire format specification.
For WebSocket/WSS transports, each binary frame contains one `PacketBase` protobuf payload without the TCP length header.
Protocol compatibility is tracked separately from language package versions. See [VERSIONING.md](VERSIONING.md). Even while packages are not published, the checked-in implementations must preserve the documented protocol contract.
---
## Implementations
@ -40,6 +42,8 @@ For WebSocket/WSS transports, each binary frame contains one `PacketBase` protob
| Python | Planned | `python/` | Server, tooling, scripting |
| Rust | Planned | `rust/` | High-performance server, embedded |
New language implementations should start from [PORTING_GUIDE.md](PORTING_GUIDE.md) and the templates in [templates/language/](templates/language/). Mark an implementation available only after its same-language tests and cross-language tests pass.
---
## Quick Start (Dart)
@ -80,20 +84,14 @@ await client.send(MyMessage()..text = 'hello');
## Adding Message Types
Edit `dart/lib/src/packets/message_common.proto` and regenerate:
Edit the canonical proto at `dart/lib/src/packets/message_common.proto`, then regenerate all checked-in bindings:
```bash
cd dart
dart pub global activate protoc_plugin
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
tools/generate_proto.sh
tools/check_proto_sync.sh
```
For Go, keep `go/packets/message_common.proto` in sync with the canonical Dart proto. The Go copy includes `option go_package`, then regenerate from the Go module root:
```bash
cd go
protoc --go_out=. --go_opt=paths=source_relative packets/message_common.proto
```
The Go proto copy is allowed to keep only its Go-specific `option go_package` difference. `tools/check_proto_sync.sh` fails with a diff when the message schema drifts.
---
@ -169,6 +167,8 @@ Go also provides:
## Running Tests
Local commands are documented here for development and troubleshooting. Continuous verification is expected to run in an external tool, with Jenkins planned to execute the full Dart, Go, and cross-language test suite.
```bash
cd dart
dart pub get
@ -179,3 +179,9 @@ dart test
cd go
go test ./...
```
When proto files change, also run:
```bash
tools/check_proto_sync.sh
```

62
VERSIONING.md Normal file
View file

@ -0,0 +1,62 @@
# Versioning
Toki Socket tracks protocol compatibility separately from language package versions.
## Current Versions
- Protocol version: `0.1`
- Dart package version: see `dart/pubspec.yaml`
- Go module version: repository tag based
`0.1` is the current compatibility contract for the checked-in Dart and Go implementations. The wire format does not carry a protocol version field yet; compatibility is verified by shared proto definitions, unit tests, and cross-language tests.
## Protocol Version
The protocol version describes the wire-format and behavior contract that every language implementation must satisfy:
- TCP uses a 4-byte big-endian payload length followed by one `PacketBase` protobuf payload.
- WebSocket and WSS use one binary frame per `PacketBase` protobuf payload.
- `PacketBase.typeName` routes the inner message.
- `nonce` increments for every outbound packet on a connection.
- `responseNonce` links a response packet to the request nonce.
- Heartbeat behavior follows `PROTOCOL.md`.
Breaking protocol changes require a new major protocol version. Backward-compatible additions keep the same major protocol version but must include updated tests before release.
## Package Version
Each language implementation may publish on its own package cadence. Package versions communicate implementation releases, bug fixes, and language-specific API changes.
A package release must state which protocol version it implements. A package version bump does not imply a protocol version bump unless the wire-format or required behavior changes.
## Breaking Protocol Changes
Examples of breaking protocol changes:
- Renaming or removing `PacketBase` fields.
- Changing `typeName` from simple proto names to fully-qualified names without a compatibility path.
- Changing `nonce` or `responseNonce` semantics.
- Changing heartbeat request/response timing or echo behavior in a way that disconnects older peers.
- Changing TCP framing, byte order, max packet handling, or WebSocket binary-frame rules.
## Non-breaking Protocol Changes
Examples of non-breaking changes:
- Adding a new protobuf message type.
- Adding optional fields to application messages when older peers can ignore them.
- Adding a new language implementation that passes the existing protocol and cross-language tests.
- Tightening tests or documentation without changing wire behavior.
Backward-compatible message additions require updated parser maps and cross-language tests before release.
## New Language Compatibility
A new language implementation must target the current protocol version unless its README explicitly says otherwise. Before it is listed as available, it must:
- Generate protobuf bindings from the canonical schema.
- Use the same `typeName` values as Dart and Go.
- Implement TCP, TLS+TCP, WS, and WSS framing consistently where the language runtime supports them.
- Implement request-response correlation with `nonce` and `responseNonce`.
- Handle heartbeat automatically inside the client/server core.
- Pass same-language tests and cross-language tests against at least one available implementation.

View file

@ -4,6 +4,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'package:protobuf/protobuf.dart';
import 'packets/message_common.pb.dart';
import 'transport.dart';
abstract class Communicator {
Map<String, IDataHandler> _handlerDic = {};
@ -11,6 +12,7 @@ abstract class Communicator {
Map<int, _PendingRequest> _pendingRequests = {};
late Map<String, GeneratedMessage Function(List<int>)> _instanceGenerator;
Future<void> _outboundWrite = Future.value();
Transport? _transport;
/// Monotonically increasing nonce. Shared by send / sendRequest / response.
int _nonce = 0;
@ -29,8 +31,10 @@ abstract class Communicator {
Communicator();
void initialize(
Map<String, GeneratedMessage Function(List<int>)> instanceGenerator) {
Map<String, GeneratedMessage Function(List<int>)> instanceGenerator,
{Transport? transport}) {
_instanceGenerator = instanceGenerator;
_transport = transport ?? _LegacyCommunicatorTransport(this);
}
T Function(List<int>) getGenerator<T extends GeneratedMessage>(String type) {
@ -41,12 +45,21 @@ abstract class Communicator {
return _instanceGenerator[type] as T Function(List<int>);
}
/// Transport-specific framing and write. Implemented by each client subclass.
Future<void> transmitPacket(PacketBase base);
/// Deprecated compatibility path for subclasses that still override writes.
///
/// New clients should pass a [Transport] to [initialize] instead.
@Deprecated('Pass a Transport to initialize instead.')
Future<void> transmitPacket(PacketBase base) {
return Future.error(StateError('transport is not initialized'));
}
/// Serializes writes so stream transports do not interleave packets.
Future<void> queuePacket(PacketBase base) {
final write = _outboundWrite.then((_) => transmitPacket(base));
final transport = _transport;
if (transport == null) {
return Future.error(StateError('transport is not initialized'));
}
final write = _outboundWrite.then((_) => transport.writePacket(base));
_outboundWrite = write.catchError((_) {});
return write;
}
@ -175,6 +188,20 @@ abstract class Communicator {
}
}
class _LegacyCommunicatorTransport implements Transport {
final Communicator _communicator;
_LegacyCommunicatorTransport(this._communicator);
@override
Future<void> writePacket(PacketBase base) {
return _communicator.transmitPacket(base);
}
@override
Future<void> close() async {}
}
abstract class IDataHandler {
void onMessage(List<int> data);
}

View file

@ -7,10 +7,14 @@ import 'dart:typed_data';
import 'package:protobuf/protobuf.dart';
import 'base_client.dart';
import 'packets/message_common.pb.dart';
import 'transport.dart';
const int maxPacketSize = 64 << 20;
abstract class ProtobufClient extends BaseClient<ProtobufClient> {
final int _headerSize = 4;
final Socket _socket;
late final _TcpSocketTransport _transport;
/// Plain TCP connection.
static Future<Socket> connect(String host, int port) =>
@ -29,11 +33,12 @@ abstract class ProtobufClient extends BaseClient<ProtobufClient> {
Map<String, GeneratedMessage Function(List<int>)> parserMap) {
initSelf(this);
initHeartbeat(heartbeatIntervalTime, heartbeatWaitTime);
_transport = _TcpSocketTransport(_socket, _headerSize);
isAlive = true;
parserMap.addAll({
(HeartBeat).toString(): HeartBeat.fromBuffer,
});
super.initialize(parserMap);
super.initialize(parserMap, transport: _transport);
_socket.listen(onData, onError: onError).asFuture<void>().then((_) {
close();
});
@ -67,6 +72,10 @@ abstract class ProtobufClient extends BaseClient<ProtobufClient> {
_arrivedData.clear();
return;
}
if (length < 0 || length > maxPacketSize) {
unawaited(close());
return;
}
_length = length;
}
@ -86,7 +95,19 @@ abstract class ProtobufClient extends BaseClient<ProtobufClient> {
}
@override
Future<void> transmitPacket(PacketBase base) async {
Future<void> closeTransport() async {
await _transport.close();
}
}
class _TcpSocketTransport implements Transport {
final Socket _socket;
final int _headerSize;
_TcpSocketTransport(this._socket, this._headerSize);
@override
Future<void> writePacket(PacketBase base) async {
final baseBytes = base.writeToBuffer();
final header = Uint8List(_headerSize)
..buffer.asByteData().setInt32(0, baseBytes.length);
@ -95,7 +116,7 @@ abstract class ProtobufClient extends BaseClient<ProtobufClient> {
}
@override
Future<void> closeTransport() async {
Future<void> close() async {
try {
await _socket.close();
_socket.destroy();

View file

@ -0,0 +1,6 @@
import 'packets/message_common.pb.dart';
abstract interface class Transport {
Future<void> writePacket(PacketBase base);
Future<void> close();
}

View file

@ -7,9 +7,11 @@ import 'dart:typed_data';
import 'package:protobuf/protobuf.dart';
import 'base_client.dart';
import 'packets/message_common.pb.dart';
import 'transport.dart';
abstract class WsProtobufClient extends BaseClient<WsProtobufClient> {
final WebSocket _ws;
late final _WebSocketTransport _transport;
/// Plain WebSocket connection.
static Future<WebSocket> connect(String host, int port,
@ -27,9 +29,10 @@ abstract class WsProtobufClient extends BaseClient<WsProtobufClient> {
Map<String, GeneratedMessage Function(List<int>)> parserMap) {
initSelf(this);
initHeartbeat(heartbeatIntervalTime, heartbeatWaitTime);
_transport = _WebSocketTransport(_ws);
isAlive = true;
parserMap.addAll({(HeartBeat).toString(): HeartBeat.fromBuffer});
super.initialize(parserMap);
super.initialize(parserMap, transport: _transport);
_ws.listen(_onMessage, onError: onError, onDone: close);
addListener(onHeartBeat);
sendHeartBeat();
@ -46,12 +49,23 @@ abstract class WsProtobufClient extends BaseClient<WsProtobufClient> {
}
@override
Future<void> transmitPacket(PacketBase base) async {
Future<void> closeTransport() async {
await _transport.close();
}
}
class _WebSocketTransport implements Transport {
final WebSocket _ws;
_WebSocketTransport(this._ws);
@override
Future<void> writePacket(PacketBase base) async {
_ws.add(base.writeToBuffer());
}
@override
Future<void> closeTransport() async {
Future<void> close() async {
try {
await _ws.close();
} catch (_) {

View file

@ -1,6 +1,7 @@
library toki_socket;
export 'src/communicator.dart';
export 'src/transport.dart';
export 'src/base_client.dart';
export 'src/protobuf_client.dart';
export 'src/protobuf_server.dart';

View file

@ -3,19 +3,14 @@ import 'package:test/test.dart';
import 'package:toki_socket/toki_socket.dart';
class _FakeCommunicator extends Communicator {
final sentPackets = <PacketBase>[];
final _FakeTransport transport = _FakeTransport();
_FakeCommunicator() {
isAlive = true;
initialize({
TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer,
HeartBeat.getDefault().info_.qualifiedMessageName: HeartBeat.fromBuffer,
});
}
@override
Future<void> transmitPacket(PacketBase base) async {
sentPackets.add(base);
}, transport: transport);
}
void closeForTest() {
@ -35,6 +30,23 @@ class _FakeCommunicator extends Communicator {
}
}
class _FakeTransport implements Transport {
final sentPackets = <PacketBase>[];
Object? error;
@override
Future<void> writePacket(PacketBase base) async {
final error = this.error;
if (error != null) {
throw error;
}
sentPackets.add(base);
}
@override
Future<void> close() async {}
}
void main() {
group('Communicator protocol guards', () {
test('response typeName mismatch completes sendRequest with error',
@ -47,7 +59,7 @@ void main() {
);
await Future<void>.delayed(Duration.zero);
final requestNonce = communicator.sentPackets.single.nonce;
final requestNonce = communicator.transport.sentPackets.single.nonce;
communicator.onReceivedData(
HeartBeat.getDefault().info_.qualifiedMessageName,
HeartBeat().writeToBuffer(),
@ -113,5 +125,25 @@ void main() {
throwsA(isA<StateError>()),
);
});
test('queuePacket uses injected transport', () async {
final communicator = _FakeCommunicator();
final packet = PacketBase()
..typeName = TestData.getDefault().info_.qualifiedMessageName
..nonce = 1
..data = (TestData()..index = 7).writeToBuffer();
await communicator.queuePacket(packet);
expect(communicator.transport.sentPackets, hasLength(1));
expect(communicator.transport.sentPackets.single, same(packet));
final error = StateError('write failed');
communicator.transport.error = error;
await expectLater(
communicator.queuePacket(PacketBase()..typeName = 'TestData'),
throwsA(same(error)),
);
});
});
}

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:test/test.dart';
import 'package:toki_socket/toki_socket.dart';
@ -330,6 +331,28 @@ void main() {
);
expect(client.isAlive, isFalse);
});
test('TCP closes on oversized packet length', () async {
final rawSocket = await Socket.connect(_host, _testPort);
try {
await Future.delayed(const Duration(milliseconds: 100));
final serverClient = server.connectedClients.last;
final disconnected = Completer<void>();
serverClient.addDisconnectListener((_) {
if (!disconnected.isCompleted) disconnected.complete();
});
final header = Uint8List(4)
..buffer.asByteData().setInt32(0, maxPacketSize + 1);
rawSocket.add(header);
await rawSocket.flush();
await disconnected.future.timeout(const Duration(seconds: 2));
expect(serverClient.isAlive, isFalse);
} finally {
rawSocket.destroy();
}
});
});
group('Heartbeat timeout', () {

View file

@ -0,0 +1,124 @@
<!-- task=ai_first_improvements plan=0 tag=AI_FIRST -->
# Code Review Reference — AI_FIRST
## 개요
task=ai_first_improvements, plan=0, tag=AI_FIRST
## 구현 항목별 완료 여부
| 항목 | 완료 | 비고 |
|------|------|------|
| [AI_FIRST-1] Dart 전송 계층을 Go 기준으로 정렬 | [x] | `Transport` 추가, TCP/WS adapter 주입, TCP max packet guard 및 회귀 테스트 추가 |
| [AI_FIRST-2] Proto sync와 codegen 검증 도구 추가 | [x] | `tools/check_proto_sync.sh`, `tools/generate_proto.sh` 추가 및 README 갱신 |
| [AI_FIRST-3] 프로토콜/패키지 버전 정책 문서화 | [x] | `VERSIONING.md` 추가, `PROTOCOL.md`/README에 protocol version과 breaking 후보 명시 |
| [AI_FIRST-4] 새 언어 구현 scaffolding과 검증 체크리스트 추가 | [x] | `templates/language/` 3개 템플릿 추가, 포팅 절차 문서화 |
## 계획 대비 변경 사항
- 계획서대로 구현.
- Dart `Communicator.transmitPacket`은 즉시 제거하지 않고 deprecated fallback으로 유지했다. 기존 `initialize(parserMap)`만 호출하는 subclass 호환을 보존하면서 새 구현은 `Transport` 주입 경로를 사용한다.
- `tools/check_proto_sync.sh`는 `option go_package`뿐 아니라 빈 줄 차이도 정규화한다. schema drift 검사가 목적이라 포맷 공백만으로 실패하지 않게 했다.
## 주요 설계 결정
- Dart 전송 계층은 `dart/lib/src/transport.dart`의 최소 `Transport.writePacket`/`Transport.close` 인터페이스로 분리했다.
- TCP/WS client public 생성자는 유지하고 내부에서 `_TcpSocketTransport`, `_WebSocketTransport`를 만들어 `Communicator.initialize(..., transport: ...)`에 주입한다.
- TCP parser는 Go의 `MaxPacketSize = 64 << 20`와 맞춰 `maxPacketSize`를 추가했고, 음수 또는 초과 length를 받으면 `close()`를 예약한다.
- Proto sync 검사는 Dart canonical proto와 Go proto copy를 비교하되 Go 전용 `option go_package`는 허용한다.
- 새 언어 템플릿은 실제 코드를 생성하지 않고 README, 구현 체크리스트, crosstest 체크리스트만 제공한다.
## 리뷰어를 위한 체크포인트
- `dart/lib/src/communicator.dart`: `initialize`의 `transport` 선택 인자, deprecated fallback, `queuePacket`의 serialized write 경로.
- `dart/lib/src/protobuf_client.dart`: TCP transport adapter write/close 분리와 oversized/negative packet length close 처리.
- `dart/lib/src/ws_protobuf_client.dart`: WebSocket binary write/close adapter 분리.
- `tools/check_proto_sync.sh`: Go 전용 proto option 허용 범위와 diff 출력.
- `templates/language/`: 새 언어 구현에 과한 코드 구조를 강제하지 않고 필수 검증 조건만 담았는지 확인.
## 검증 결과
```
$ tools/check_proto_sync.sh
Proto schemas are in sync.
$ cd dart && dart analyze
Analyzing dart...
No issues found!
$ cd dart && dart test
00:15 +42: All tests passed!
$ cd go && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go test ./...
? toki-labs.com/toki_socket/go [no test files]
? toki-labs.com/toki_socket/go/crosstest [no test files]
? toki-labs.com/toki_socket/go/crosstest/dart_go_client [no test files]
? toki-labs.com/toki_socket/go/examples/tcp_echo [no test files]
? toki-labs.com/toki_socket/go/examples/ws_echo [no test files]
? toki-labs.com/toki_socket/go/packets [no test files]
ok toki-labs.com/toki_socket/go/test (cached)
$ find templates/language -maxdepth 1 -type f
templates/language/IMPLEMENTATION_CHECKLIST.md
templates/language/CROSSTEST_CHECKLIST.md
templates/language/README.md
$ cd dart && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH dart run crosstest/dart_go.dart
INFO typeName dart=TestData
INFO typeName go=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from dart server
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from dart server
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all dart-server/go-client crosstests passed
$ cd go && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest
INFO typeName go=TestData
INFO typeName dart=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all go-server/dart-client crosstests passed
```
참고: crosstest는 기본 sandbox에서 SDK cache 쓰기 또는 localhost bind가 차단되어 승인된 외부 실행으로 재시도했다.
---
## 코드리뷰 결과
### 종합 판정
**PASS**
> 4개 항목 모두 계획대로 구현됐고, 테스트 수(42개)와 검증 출력이 실제 코드와 일치한다. 전송 계층 분리, proto sync 도구, 버전 정책 문서, 언어 추가 템플릿 모두 품질 이슈 없음.
### 차원별 평가
| 차원 | 판정 | 비고 |
|------|------|------|
| 정확성 (Correctness) | Pass | maxPacketSize 가드, queuePacket 에러 전파, _LegacyCommunicatorTransport 위임 경로 모두 정확 |
| 완성도 (Completeness) | Pass | 계획서의 모든 체크리스트 항목 구현 확인 |
| 테스트 커버리지 | Pass | `queuePacket uses injected transport`(에러 전파 포함), `TCP closes on oversized packet length` 두 신규 테스트 모두 의미 있는 assertion |
| API 계약 | Pass | `toki_socket.dart`에 `transport.dart` export 추가, 기존 `ProtobufClient`/`WsProtobufClient` public 생성자 유지 |
| 코드 품질 | Pass | 디버그 출력·dead import·leftover TODO 없음 |
| 계획 대비 이탈 | Pass | `transmitPacket` deprecated 유지 결정이 CODE_REVIEW.md에 명시됨 |
| 검증 신뢰도 | Pass | 보고된 출력(42개 통과, crosstest PASS)이 실제 코드 로직과 일치 |
### 발견된 문제
- **[Nit]** `dart/lib/src/communicator.dart:59` — `if (transport == null)` 분기는 `initialize()` 호출 후에는 실행되지 않는 방어 코드다. `initialize()` 가 항상 non-null 값을 `_transport`에 할당하므로 실질적으로 dead branch다. 제거하거나 `assert(false, 'queuePacket called before initialize')` 로 교체 가능하지만 동작에 영향 없음.
### 다음 단계
**[PASS]** 추가 작업 불필요. 아카이브 완료.

View file

@ -0,0 +1,383 @@
<!-- task=ai_first_improvements plan=0 tag=AI_FIRST -->
# AI-first 개선 계획
## 이 파일을 읽는 구현 에이전트에게
- 각 항목의 체크리스트를 순서대로 완료한다.
- 항목 완료마다 **중간 검증** 커맨드를 실행하고 통과를 확인한다.
- 모든 항목 완료 후 **최종 검증** 커맨드를 실행한다.
- 작업이 끝나면 `CODE_REVIEW.md`의 각 섹션을 실제 구현 내용으로 채운다:
- 구현 항목별 완료 여부 체크
- 계획 대비 변경 사항 (없으면 "계획서 그대로 구현")
- 주요 설계 결정
- 리뷰어를 위한 체크포인트
- 검증 결과 (실제 실행한 명령과 출력 붙여넣기)
## 배경
현재 프로젝트는 Go와 Dart 양방향 crosstest가 통과하는 좋은 기반을 갖고 있다. 다음 단계는 AI 에이전트가 새 언어, 새 메시지 타입, 새 릴리스 규칙을 안전하게 따라갈 수 있도록 반복 작업을 명문화하고 자동화하는 것이다. CI 실행 자체는 README에 명시한 대로 Jenkins가 담당하므로, 이 계획은 저장소 내부의 구현 정리와 검증 도구, 버전 정책, 언어 추가 scaffolding에 집중한다.
---
### [AI_FIRST-1] Dart 전송 계층을 Go 기준으로 정렬
**문제**
Go는 `Transport` 인터페이스를 통해 `Communicator`가 TCP/WS 세부 구현을 모르도록 분리되어 있다. 반면 Dart는 `Communicator`가 `transmitPacket` 추상 메서드를 직접 요구하고, TCP/WS 클라이언트가 각각 heartbeat와 전송 세부를 갖는다.
```dart
// Before — dart/lib/src/communicator.dart:44
/// Transport-specific framing and write. Implemented by each client subclass.
Future<void> transmitPacket(PacketBase base);
/// Serializes writes so stream transports do not interleave packets.
Future<void> queuePacket(PacketBase base) {
final write = _outboundWrite.then((_) => transmitPacket(base));
_outboundWrite = write.catchError((_) {});
return write;
}
```
`PORTING_GUIDE.md:14`는 새 언어 구현에서 `Transport` 분리를 반드시 유지해야 한다고 말한다. Dart가 현재 레거시 패턴으로 남아 있으면 이후 AI 포팅 작업이 Go와 Dart 사이에서 서로 다른 기준을 학습하게 된다.
**해결 방법**
`dart/lib/src/transport.dart`를 새로 만들고 Dart에도 Go와 같은 최소 전송 인터페이스를 추가한다. `Communicator`는 `Transport? _transport`를 보유하고, `initialize`에서 parser map과 transport를 함께 받는 새 경로를 제공한다. 기존 `ProtobufClient`/`WsProtobufClient` public 생성자는 유지하되 내부에서 각각 TCP/WS transport adapter를 만들어 `Communicator`에 주입한다.
```dart
// After
abstract interface class Transport {
Future<void> writePacket(PacketBase base);
Future<void> close();
}
Future<void> queuePacket(PacketBase base) {
final transport = _transport;
if (transport == null) {
return Future.error(StateError('transport is not initialized'));
}
final write = _outboundWrite.then((_) => transport.writePacket(base));
_outboundWrite = write.catchError((_) {});
return write;
}
```
TCP 파서는 Go와 같은 최대 패킷 제한도 갖는다. Go는 `go/tcp_client.go:17`의 `MaxPacketSize = 64 << 20`과 `go/tcp_client.go:73`의 초과 close 처리가 있다. Dart도 같은 상수를 두고 음수/초과 길이는 즉시 `close()`로 처리한다.
```dart
// Before — dart/lib/src/protobuf_client.dart:61
final length = Uint8List.fromList(_arrivedData.sublist(0, _headerSize))
.buffer
.asByteData()
.getInt32(0);
if (length == 0) {
_length = null;
_arrivedData.clear();
return;
}
_length = length;
// After
final length = Uint8List.fromList(_arrivedData.sublist(0, _headerSize))
.buffer
.asByteData()
.getInt32(0);
if (length == 0) {
_length = null;
_arrivedData.clear();
return;
}
if (length < 0 || length > maxPacketSize) {
unawaited(close());
return;
}
_length = length;
```
**수정 파일 및 체크리스트**
- [ ] `dart/lib/src/transport.dart` — Dart 전송 인터페이스 추가
- [ ] `Transport.writePacket(PacketBase base)` 정의
- [ ] `Transport.close()` 정의
- [ ] `dart/lib/src/communicator.dart` — transport 주입 경로 추가
- [ ] 기존 `initialize(Map<...>)` 호출부와 호환되는 migration 경로 설계
- [ ] `queuePacket`이 `transport.writePacket`을 사용하도록 변경
- [ ] 기존 `transmitPacket` 추상 메서드 제거 또는 deprecated 호환 경로 유지 여부를 `CODE_REVIEW.md`에 기록
- [ ] `dart/lib/src/protobuf_client.dart` — TCP transport adapter 적용
- [ ] `maxPacketSize = 64 << 20` 추가
- [ ] 음수 길이와 최대 크기 초과 시 `close()` 호출
- [ ] `_socket.add`/`flush` 로직을 TCP transport adapter로 이동
- [ ] `dart/lib/src/ws_protobuf_client.dart` — WS transport adapter 적용
- [ ] `_ws.add(base.writeToBuffer())` 로직을 WS transport adapter로 이동
- [ ] close 로직을 adapter와 client close 순서가 충돌하지 않게 정리
- [ ] `dart/lib/toki_socket.dart` — 필요한 경우 `transport.dart` export
**테스트 작성**
| 파일 | 테스트 이름 | 검증 목표 |
|------|-------------|-----------|
| `dart/test/communicator_test.dart` | `queuePacket uses injected transport` | fake transport가 받은 `PacketBase`와 에러 전파 확인 |
| `dart/test/socket_test.dart` | `TCP closes on oversized packet length` | 수동 socket으로 64 MiB 초과 header 전송 후 disconnect 확인 |
내부 리팩터링이지만 전송 계층과 TCP 파서 동작이 바뀌므로 회귀 테스트를 추가한다.
**중간 검증**
```bash
# 정적 분석
cd dart && dart analyze
# 테스트
cd dart && dart test
```
Expected: 기존 40개 + 신규 2개 = 총 42개 통과
---
### [AI_FIRST-2] Proto sync와 codegen 검증 도구 추가
**문제**
`PROTOCOL.md:182`는 Dart proto를 canonical source로 지정하고, `PROTOCOL.md:185`는 Go copy를 수동으로 sync해야 한다고 설명한다. README도 같은 수동 절차를 반복한다.
```markdown
// Before — README.md:81
## Adding Message Types
Edit `dart/lib/src/packets/message_common.proto` and regenerate:
```
이 상태에서는 AI 에이전트나 사람이 proto를 바꾼 뒤 Go copy, generated Dart, generated Go 중 하나를 빠뜨리기 쉽다.
**해결 방법**
`tools/` 아래에 proto 검증과 재생성 스크립트를 추가한다. Go proto에는 `option go_package`만 허용되는 차이로 남기고, 나머지 message body가 Dart canonical proto와 일치하는지 검사한다. 재생성 스크립트는 Dart와 Go 생성 명령을 한 곳에 모은다.
```bash
# After
tools/check_proto_sync.sh
tools/generate_proto.sh
```
`README.md`의 Adding Message Types 섹션은 수동 명령 나열에서 도구 기반 절차로 바꾼다.
````markdown
// After
## Adding Message Types
Edit `dart/lib/src/packets/message_common.proto`, then run:
```bash
tools/generate_proto.sh
tools/check_proto_sync.sh
```
````
**수정 파일 및 체크리스트**
- [ ] `tools/check_proto_sync.sh` — Dart/Go proto sync 검사
- [ ] Dart canonical proto와 Go proto를 읽는다
- [ ] Go 전용 `option go_package` 라인을 제외한 schema body 비교
- [ ] mismatch 시 diff와 함께 non-zero exit
- [ ] `tools/generate_proto.sh` — Dart/Go protobuf 재생성
- [ ] Dart `protoc --dart_out=lib/src/packets ...` 실행
- [ ] Go `protoc --go_out=. --go_opt=paths=source_relative ...` 실행
- [ ] 필요한 binary가 없을 때 명확한 에러 메시지 출력
- [ ] `README.md` — Adding Message Types와 Running Tests 갱신
- [ ] proto 변경 절차를 `tools/generate_proto.sh` 중심으로 정리
- [ ] Jenkins가 외부 도구에서 전체 검증을 실행한다는 문구 유지
- [ ] `.gitignore` — 필요 시 generated temp/output 제외
**테스트 작성**
스크립트 추가이므로 별도 Dart/Go 단위 테스트는 불필요하다. 대신 `tools/check_proto_sync.sh`를 직접 실행하고, 최종 검증 명령에 포함한다.
**중간 검증**
```bash
# proto sync
tools/check_proto_sync.sh
# 기존 테스트
cd dart && dart test
cd go && go test ./...
```
Expected: proto sync 성공, Dart 40개 이상 통과, Go 전체 테스트 통과
---
### [AI_FIRST-3] 프로토콜/패키지 버전 정책 문서화
**문제**
Dart 패키지는 `version: 0.1.0`을 갖지만 `publish_to: none`이라 배포 정책이 닫혀 있고, 프로토콜 자체의 호환성 버전은 별도로 없다.
```yaml
// Before — dart/pubspec.yaml:1
name: toki_socket
description: Multi-language standard socket protocol library. Dart implementation.
version: 0.1.0
publish_to: none
```
프로토콜 필드 추가, `typeName` 규칙 변경, heartbeat 의미 변경 같은 변화가 언어별 패키지 버전과 어떻게 연결되는지 기준이 없다. AI-first 포팅에서는 이 기준이 없으면 새 언어 구현체가 어느 프로토콜 버전을 만족해야 하는지 판단하기 어렵다.
**해결 방법**
`VERSIONING.md`를 추가하고 protocol compatibility, package version, breaking change 기준을 분리한다. `PROTOCOL.md` 상단에는 현재 protocol version을 명시하고, README에서는 버전 정책 문서로 연결한다. 당장은 wire format을 바꾸지 않으므로 proto message에 version field를 추가하지 않는다.
```markdown
// After
# Versioning
- Protocol version: wire-format compatibility contract.
- Package version: language implementation release version.
- Breaking protocol changes require a new major protocol version.
- Backward-compatible message additions require crosstest updates before release.
```
**수정 파일 및 체크리스트**
- [ ] `VERSIONING.md` — 새 버전 정책 문서
- [ ] protocol version과 package version의 차이 정의
- [ ] breaking/non-breaking 예시 작성
- [ ] 새 언어 구현체가 따라야 할 최소 호환성 기준 작성
- [ ] `PROTOCOL.md` — protocol version 명시
- [ ] 문서 상단에 `Current protocol version: 0.1` 추가
- [ ] `typeName`, `nonce`, `responseNonce`, heartbeat 변경은 breaking 후보임을 연결
- [ ] `README.md` — Versioning 섹션 추가
- [ ] `VERSIONING.md` 링크
- [ ] 패키지 배포 전에도 프로토콜 호환성 기준은 유지한다는 문구 추가
- [ ] `dart/pubspec.yaml` — 변경 없음
- [ ] 실제 패키지 배포를 하지 않으므로 `publish_to: none`은 유지
**테스트 작성**
문서 정책 추가라 단위 테스트는 불필요하다. 다만 최종 검증에서 기존 테스트와 crosstest를 실행해 문서화한 현재 호환성 상태가 실제로 유지되는지 확인한다.
**중간 검증**
```bash
# 문서 변경 후 기본 검증
cd dart && dart analyze
cd go && go test ./...
```
Expected: analyzer 이슈 없음, Go 전체 테스트 통과
---
### [AI_FIRST-4] 새 언어 구현 scaffolding과 검증 체크리스트 추가
**문제**
`PORTING_GUIDE.md`는 C#, Kotlin, Swift, Python, Rust 구현 지침을 잘 설명하지만 구현 시작점과 완료 조건은 문서 안에 흩어져 있다. `skills/add-crosstest-language/SKILL.md`에는 crosstest 규칙이 있으나, 일반 구현자가 어디서 어떤 파일을 만들어 시작해야 하는지 scaffold가 없다.
```markdown
// Before — PORTING_GUIDE.md:23
## C# (Unity / .NET)
### 핵심 매핑
```
AI 에이전트가 새 언어를 추가할 때 매번 디렉터리 구조, README, crosstest runner, parser map 예제를 새로 설계하게 된다.
**해결 방법**
`templates/language/` 아래에 새 언어 구현체가 채워 넣을 README, protocol checklist, crosstest checklist 템플릿을 둔다. 실제 코드는 언어별로 달라 템플릿이 과하게 코드를 생성하지 않게 하고, 반드시 만족해야 하는 파일 구조와 검증 명령만 고정한다.
```text
// After
templates/language/README.md
templates/language/IMPLEMENTATION_CHECKLIST.md
templates/language/CROSSTEST_CHECKLIST.md
```
`PORTING_GUIDE.md`에는 "새 언어 추가 절차" 섹션을 추가해서 템플릿, proto generation, crosstest 작성 순서를 연결한다.
**수정 파일 및 체크리스트**
- [ ] `templates/language/README.md` — 새 언어 package README 템플릿
- [ ] transport, parser map, client/server, tests 항목 placeholder 포함
- [ ] `templates/language/IMPLEMENTATION_CHECKLIST.md` — 구현 완료 조건
- [ ] `Transport`, `Communicator`, TCP, WS, TLS/WSS, heartbeat, request-response 항목 포함
- [ ] typeName compatibility 확인 항목 포함
- [ ] `templates/language/CROSSTEST_CHECKLIST.md` — 크로스언어 테스트 완료 조건
- [ ] send-push, single request, concurrent request, TCP, WS 항목 포함
- [ ] 양방향 테스트와 PASS/FAIL line format 요구
- [ ] `PORTING_GUIDE.md` — 새 언어 추가 절차 추가
- [ ] templates 사용 방법 링크
- [ ] `skills/add-crosstest-language/SKILL.md`의 runner 배치 규칙과 충돌하지 않게 설명
- [ ] `README.md` — Multi-language 확장 안내 추가
- [ ] 새 언어는 `PORTING_GUIDE.md`와 templates를 먼저 따르도록 안내
**테스트 작성**
템플릿과 문서 추가이므로 단위 테스트는 불필요하다. 하지만 Markdown 링크와 경로가 실제로 존재하는지 `find`/`rg`로 확인하고, 기존 Dart/Go 테스트를 유지 검증으로 실행한다.
**중간 검증**
```bash
# 템플릿 경로 확인
find templates/language -maxdepth 1 -type f
# 기존 테스트
cd dart && dart test
cd go && go test ./...
```
Expected: 템플릿 3개 존재, Dart 40개 이상 통과, Go 전체 테스트 통과
---
## 의존 관계 및 구현 순서
```text
[AI_FIRST-1] → [AI_FIRST-2] → [AI_FIRST-3] → [AI_FIRST-4]
```
`AI_FIRST-1`은 Dart 구현을 기준 구조에 맞추는 작업이라 이후 문서와 템플릿의 예시가 흔들리지 않게 먼저 끝낸다. `AI_FIRST-2`는 proto 변경 루프를 안정화하고, `AI_FIRST-3`은 안정화된 현재 wire contract를 버전 정책으로 고정한다. `AI_FIRST-4`는 앞의 결정들을 새 언어 구현 템플릿에 반영한다.
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `dart/lib/src/transport.dart` | AI_FIRST-1 |
| `dart/lib/src/communicator.dart` | AI_FIRST-1 |
| `dart/lib/src/protobuf_client.dart` | AI_FIRST-1 |
| `dart/lib/src/ws_protobuf_client.dart` | AI_FIRST-1 |
| `dart/lib/toki_socket.dart` | AI_FIRST-1 |
| `dart/test/communicator_test.dart` | AI_FIRST-1 |
| `dart/test/socket_test.dart` | AI_FIRST-1 |
| `tools/check_proto_sync.sh` | AI_FIRST-2 |
| `tools/generate_proto.sh` | AI_FIRST-2 |
| `README.md` | AI_FIRST-2, AI_FIRST-3, AI_FIRST-4 |
| `VERSIONING.md` | AI_FIRST-3 |
| `PROTOCOL.md` | AI_FIRST-3 |
| `templates/language/README.md` | AI_FIRST-4 |
| `templates/language/IMPLEMENTATION_CHECKLIST.md` | AI_FIRST-4 |
| `templates/language/CROSSTEST_CHECKLIST.md` | AI_FIRST-4 |
| `PORTING_GUIDE.md` | AI_FIRST-4 |
---
## 최종 검증
```bash
# 1. Proto sync
tools/check_proto_sync.sh
# 2. Dart 정적 분석과 테스트
cd dart && dart analyze
cd dart && dart test
# 3. Go 테스트
cd go && go test ./...
# 4. Cross-language tests
cd dart && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH dart run crosstest/dart_go.dart
cd go && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest
```
Expected outcome: proto sync 성공, Dart 기존 40개 + 신규 2개 이상 통과, Go 전체 테스트 통과, Dart-server/Go-client 및 Go-server/Dart-client crosstest 통과

View file

@ -0,0 +1,44 @@
# Cross-language Test Checklist
Use the runner layout from `skills/add-crosstest-language/SKILL.md`.
## Runner Layout
- [ ] Server-language orchestrator lives under `<server-language>/crosstest/`.
- [ ] Client-language subprocess helper lives under `<client-language>/crosstest/`.
- [ ] No root-level package metadata is added only for crosstests.
- [ ] Subprocess paths resolve from the package root or runner source location.
## Required Output
The client helper prints its protocol type name before scenarios:
```text
INFO typeName <language>=TestData
```
Each scenario prints exactly one result line:
```text
PASS scenario=N detail=...
FAIL scenario=N error=...
```
The orchestrator fails when an expected scenario is missing, any `FAIL` line appears, or the subprocess exits non-zero.
## Required Scenarios
Run each scenario for TCP and WebSocket:
- [ ] Scenario 1: client sends fire-and-forget `TestData`; server validates `index` and `message`.
- [ ] Scenario 2: server pushes `TestData(index=200, message=push from ... server)` to the client.
- [ ] Scenario 3: client `sendRequest` receives `index=req.index*2` and `message=echo: req.message`.
- [ ] Scenario 4: multiple concurrent `sendRequest` calls verify nonce and response routing.
Split protocols into separate send-push and request phases if a communicator forbids normal listener and request listener registration for the same message type.
## Review Notes
- [ ] Record the exact commands used to run both directions.
- [ ] Record fixed ports and why they do not conflict with existing tests.
- [ ] Record any unsupported transport or platform caveat.

View file

@ -0,0 +1,43 @@
# Implementation Checklist
Target protocol version: `0.1`
## Protocol Core
- [ ] Generated protobuf bindings from the canonical schema.
- [ ] Parser map registers application messages by protocol-compatible `typeName`.
- [ ] Built-in `HeartBeat` registration is owned by the framework.
- [ ] `PacketBase.typeName`, `nonce`, `data`, and `responseNonce` semantics match `PROTOCOL.md`.
- [ ] Same `typeName` cannot be registered for both normal listener and request listener on one connection.
## Transport
- [ ] `Transport` abstraction exposes only packet write and close operations.
- [ ] `Communicator` does not know whether the connection is TCP, TLS+TCP, WS, or WSS.
- [ ] TCP framing writes a 4-byte big-endian length followed by one `PacketBase` protobuf payload.
- [ ] TCP reader rejects zero-length packets as no-op and closes on invalid or oversized lengths.
- [ ] WebSocket reader/writer uses one binary frame per `PacketBase` payload.
- [ ] TLS+TCP and WSS are supported or explicitly marked unsupported with a reason.
## Lifecycle
- [ ] Close is idempotent.
- [ ] Pending requests complete with an error when the connection closes.
- [ ] Writes are serialized so packet bytes cannot interleave.
- [ ] Read, write, heartbeat, and close errors converge on the same disconnect path.
## Features
- [ ] Fire-and-forget send.
- [ ] Request-response send with timeout or cancellation.
- [ ] Concurrent requests route by `responseNonce`.
- [ ] Heartbeat sends after inactivity and closes after missing response.
- [ ] Server broadcast, if the language package exposes server helpers.
## Verification
- [ ] Formatter/linter passes.
- [ ] Same-language TCP tests pass.
- [ ] Same-language WS tests pass.
- [ ] TLS/WSS tests pass where supported.
- [ ] Cross-language tests pass in both server/client directions.

View file

@ -0,0 +1,41 @@
# <Language> Toki Socket
Status: planned
This package implements Toki Socket protocol version `0.1` for <Language>.
## Required Structure
Use this package README to document the concrete files for:
- Transport abstraction: TCP, TLS+TCP, WS, and WSS implementations.
- Communicator core: parser map, listener routing, request-response correlation, and serialized writes.
- Client/server layer: connection lifecycle, close behavior, and heartbeat ownership.
- Generated protobuf bindings from `dart/lib/src/packets/message_common.proto`.
- Unit tests and cross-language tests.
## Proto Generation
Document the package-local proto generation command here. The generated schema must match the canonical Dart proto except for language-specific generator options.
```bash
# <command to generate protobuf bindings>
```
After generation, run from the repository root:
```bash
tools/check_proto_sync.sh
```
## Validation
Before marking this implementation available, document and run:
```bash
# <formatter or linter>
# <unit tests>
# <crosstest command>
```
The implementation must pass same-language tests and cross-language tests against at least one available implementation.

38
tools/check_proto_sync.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
dart_proto="$repo_root/dart/lib/src/packets/message_common.proto"
go_proto="$repo_root/go/packets/message_common.proto"
if [[ ! -f "$dart_proto" ]]; then
echo "Missing canonical Dart proto: $dart_proto" >&2
exit 1
fi
if [[ ! -f "$go_proto" ]]; then
echo "Missing Go proto copy: $go_proto" >&2
exit 1
fi
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
normalize_proto() {
awk '
/^[[:space:]]*option go_package[[:space:]]*=.*;[[:space:]]*$/ { next }
/^[[:space:]]*$/ { next }
{ print }
' "$1"
}
normalize_proto "$dart_proto" >"$tmp_dir/dart.proto"
normalize_proto "$go_proto" >"$tmp_dir/go.proto"
if ! cmp -s "$tmp_dir/dart.proto" "$tmp_dir/go.proto"; then
echo "Proto schema mismatch: go/packets/message_common.proto must match the Dart canonical proto except option go_package." >&2
diff -u "$tmp_dir/dart.proto" "$tmp_dir/go.proto" >&2
exit 1
fi
echo "Proto schemas are in sync."

30
tools/generate_proto.sh Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
require_binary() {
local binary="$1"
local hint="$2"
if ! command -v "$binary" >/dev/null 2>&1; then
echo "Missing required binary: $binary" >&2
echo "$hint" >&2
exit 1
fi
}
require_binary protoc "Install Protocol Buffers compiler and ensure protoc is on PATH."
require_binary protoc-gen-dart "Install Dart plugin: dart pub global activate protoc_plugin, then add pub-cache/bin to PATH."
require_binary protoc-gen-go "Install Go plugin: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest, then add GOPATH/bin to PATH."
(
cd "$repo_root/dart"
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
)
(
cd "$repo_root/go"
protoc --go_out=. --go_opt=paths=source_relative packets/message_common.proto
)
"$repo_root/tools/check_proto_sync.sh"