fix: resolve 24 proto definition mismatches against Node fixtures

fixes #164

Mismatches fixed:
- analytics-channels: ChannelAnalyticsSummary.hash string -> int32
- analytics-rf: PayloadTypeEntry.type -> optional int32 (can be null)
- bulk-health: flatten BulkHealthEntry (remove .node nesting)
- node-analytics: TimeBucket field label -> bucket (keep both as optional)
- observer-analytics: recentPackets Transmission -> Observation
- packet-detail: ByteRange add string color field
- websocket-message: DecodedResult add transportCodes, raw, routeTypeName;
  flatten payload to DecodedFlatPayload; packet -> Observation
- validate-protos: bare-array wrapping note downgraded to WARNING

Validator now reports 0 errors across all 33 fixtures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbot
2026-03-27 14:52:02 -07:00
parent 8414015b2c
commit 3c53680e7c
8 changed files with 91 additions and 23 deletions

View File

@@ -42,8 +42,8 @@ message ScatterPoint {
// Payload type name + count entry.
message PayloadTypeEntry {
// Payload type number.
int32 type = 1;
// Payload type number (null when unknown).
optional int32 type = 1;
// Human-readable type name.
string name = 2;
// Observation count.
@@ -250,8 +250,8 @@ message TopologyResponse {
// Channel summary in analytics context.
message ChannelAnalyticsSummary {
// Channel identifier.
string hash = 1;
// Channel identifier (numeric hash).
int32 hash = 1;
// Channel display name.
string name = 2;
// Total messages.

View File

@@ -77,9 +77,12 @@ message SignalStats {
// Generic label + count pair for time-series and distribution charts.
// Used in activity timelines, observer analytics timelines, etc.
// Node analytics uses `bucket`, observer analytics uses `label`.
message TimeBucket {
// Time label (ISO timestamp, hour string, or category label).
string label = 1;
// Time label used by observer analytics (e.g. "Sat 12 AM").
optional string label = 1;
// Count in this bucket.
int32 count = 2;
// ISO timestamp used by node analytics (e.g. "2026-03-21T21:00:00Z").
optional string bucket = 3;
}

View File

@@ -11,17 +11,26 @@ option go_package = "github.com/meshcore-analyzer/proto/v1";
// Full decoded result: header + path + payload.
message DecodedResult {
DecodedHeader header = 1;
// Transport code bytes (null if not a TRANSPORT route).
optional DecodedTransportCodes transport_codes = 6 [json_name = "transportCodes"];
DecodedPath path = 2;
// Type-specific decoded payload.
// NOTE: Proto3 JSON serializes oneof with a wrapper key (e.g. {"advert":{...}}).
// Go serialization layer must flatten this to match the spec's flat payload shape.
DecodedPayload payload = 3;
// Flat decoded payload (type-discriminated by payload.type).
DecodedFlatPayload payload = 3;
// Raw hex string of the entire packet.
optional string raw = 4;
}
// Transport code pair for TRANSPORT-routed packets.
message DecodedTransportCodes {
repeated int32 codes = 1;
}
// Parsed packet header (first byte).
message DecodedHeader {
// Route type: 0=DIRECT, 1=FLOOD, 2=reserved, 3=TRANSPORT.
int32 route_type = 1 [json_name = "routeType"];
// Human-readable route type name: "DIRECT", "FLOOD", "TRANSPORT".
optional string route_type_name = 5 [json_name = "routeTypeName"];
// Payload type: 0=REQ .. 11=CONTROL (see Payload Type Reference).
int32 payload_type = 2 [json_name = "payloadType"];
// Payload format version.
@@ -40,7 +49,51 @@ message DecodedPath {
int32 hash_count = 3 [json_name = "hashCount"];
}
// ─── Payload Oneof ─────────────────────────────────────────────────────────────
// ─── Flat Payload (used in WS broadcast / decoded result) ──────────────────────
// Node.js returns a flat payload object with a `type` discriminator string.
// All type-specific fields are optional — only the relevant ones are populated.
// Decoded advert flags (from AdvertDataHelpers.h).
message AdvertFlags {
// Raw flags bitmask value.
int32 raw = 1;
// Advert type code.
int32 type = 2;
// Supports chat.
bool chat = 3;
// Is a repeater.
bool repeater = 4;
// Is a room server.
bool room = 5;
// Is a sensor.
bool sensor = 6;
// Includes GPS coordinates.
bool has_location = 7 [json_name = "hasLocation"];
// Includes node name.
bool has_name = 8 [json_name = "hasName"];
}
// Flat decoded payload — all payload type fields merged, discriminated by `type`.
message DecodedFlatPayload {
// Payload type name: "ADVERT", "TXT_MSG", "GRP_TXT", etc.
optional string type = 1;
// --- ADVERT fields ---
optional string pub_key = 2 [json_name = "pubKey"];
optional int64 timestamp = 3;
optional string timestamp_iso = 4 [json_name = "timestampISO"];
optional string signature = 5;
optional AdvertFlags flags = 6;
optional double lat = 7;
optional double lon = 8;
optional string name = 9;
// --- TXT_MSG / GRP_TXT fields ---
optional string text = 10;
optional string sender = 11;
optional string channel = 12;
optional int64 sender_timestamp = 13 [json_name = "sender_timestamp"];
}
// ─── Payload Oneof (legacy / typed-discriminated) ──────────────────────────────────
// Type-discriminated decoded payload.
message DecodedPayload {

View File

@@ -101,15 +101,22 @@ message NodeSearchResponse {
}
// GET /api/nodes/bulk-health — bulk health summary for analytics dashboard.
// NOTE: The API currently returns a bare JSON array with flat node fields.
// Go serialization should flatten node fields to top level for backward compat.
// NOTE: The API returns a bare JSON array with flat node fields at top level.
message BulkHealthEntry {
// Node identification and location (reuses Node — DO NOT duplicate fields).
Node node = 1;
// Node public key (flat, not nested in a sub-message).
string public_key = 1 [json_name = "public_key"];
// Node display name.
optional string name = 2;
// Node role: "repeater", "room", "companion", "sensor".
optional string role = 3;
// GPS latitude (null if unknown).
optional double lat = 4;
// GPS longitude (null if unknown).
optional double lon = 5;
// Aggregate packet stats.
NodeStats stats = 2;
NodeStats stats = 6;
// Per-observer signal quality.
repeated NodeObserverStats observers = 3;
repeated NodeObserverStats observers = 7;
}
// Wrapper for the bulk-health array response.

View File

@@ -99,6 +99,6 @@ message ObserverAnalyticsResponse {
repeated TimeBucket nodes_timeline = 3 [json_name = "nodesTimeline"];
// SNR distribution in labeled ranges.
repeated SnrDistributionEntry snr_distribution = 4 [json_name = "snrDistribution"];
// Last 20 enriched observations.
repeated Transmission recent_packets = 5 [json_name = "recentPackets"];
// Last 20 enriched observations (Observation-shaped, includes transmission_id).
repeated Observation recent_packets = 5 [json_name = "recentPackets"];
}

View File

@@ -137,6 +137,8 @@ message ByteRange {
string hex = 4;
// Interpreted value (may be string, number, or absent).
optional string value = 5;
// CSS color for visual highlighting.
string color = 6;
}
// Byte-level packet structure breakdown.

View File

@@ -42,8 +42,8 @@ message WSPacketData {
optional string observer_name = 8 [json_name = "observer_name"];
// JSON-stringified hops array (redundant with decoded.path.hops).
optional string path_json = 9 [json_name = "path_json"];
// Full packet object (present for MQTT/raw ingestion, may be absent for bridge messages).
optional Transmission packet = 10;
// Full packet object (Observation-shaped, includes transmission_id).
optional Observation packet = 10;
// Observation count (present when packet is included).
optional int32 observation_count = 11 [json_name = "observation_count"];
}

View File

@@ -316,6 +316,9 @@ FIELD_TYPE_TO_MESSAGE = {
'DecodedHeader': 'DecodedHeader',
'DecodedPath': 'DecodedPath',
'DecodedPayload': 'DecodedPayload',
'DecodedFlatPayload': 'DecodedFlatPayload',
'DecodedTransportCodes': 'DecodedTransportCodes',
'AdvertFlags': 'AdvertFlags',
'AdvertPayload': 'AdvertPayload',
# channel.proto
'Channel': 'Channel',
@@ -558,9 +561,9 @@ def main():
validate_object(fixture_file, data[0], message_name,
all_messages, path=f'{message_name}[0]',
mismatches=mismatches)
# Also flag the structural mismatch
# Flag structural note (serialization concern, not a field mismatch)
mismatches.append(Mismatch(
fixture_file, message_name, 'ERROR',
fixture_file, message_name, 'WARNING',
f'API returns a bare JSON array, but proto wraps it in a '
f'response message. Serialization layer must handle unwrapping.'
))