feat(schema): protobuf event registry + buf CI gate (#54)
- Add proto schemas in packages/shared-types/events/ (oo.events.v1): envelope.proto, signals.proto, integration.proto - buf.yaml with STANDARD lint + FILE breaking-change rules - .gitea/workflows/buf-check.yaml: lint + breaking check on every PR touching events/ (needs a Gitea Actions runner to execute) - scripts/buf-check.sh: local equivalent of the CI check - NormalizedEvent TS envelope gains eventId, schemaVersion, producer to align with the proto Envelope message - ml/serving/schemas.py: pydantic models mirroring the v1 proto types - nats_consumer.py: validate payloads via pydantic instead of raw .get() A field-rename PR will now fail buf breaking with exit code 100 and show the offending messages. To make a breaking change: keep the old field reserved, add the new one, bump schema_version to v2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7
packages/shared-types/events/buf.yaml
Normal file
7
packages/shared-types/events/buf.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: v1
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
25
packages/shared-types/events/oo/events/v1/envelope.proto
Normal file
25
packages/shared-types/events/oo/events/v1/envelope.proto
Normal file
@@ -0,0 +1,25 @@
|
||||
syntax = "proto3";
|
||||
package oo.events.v1;
|
||||
|
||||
import "oo/events/v1/signals.proto";
|
||||
import "oo/events/v1/integration.proto";
|
||||
|
||||
// Envelope wraps every event on the bus and on NATS JetStream.
|
||||
// Wire format: proto3 JSON (camelCase field names).
|
||||
// schema_version = "v1" — bump to "v2" only for breaking payload changes.
|
||||
message Envelope {
|
||||
string event_id = 1; // UUID assigned by bus on publish
|
||||
string occurred_at = 2; // ISO 8601
|
||||
string schema_version = 3; // "v1"
|
||||
string producer = 4; // e.g. "services/api"
|
||||
string subject = 5; // NATS-style subject: domain.entity.verb
|
||||
uint64 seq = 6; // monotonic sequence from the bus ring
|
||||
|
||||
oneof payload {
|
||||
TaskSyncedPayload task_synced = 10;
|
||||
TipServedPayload tip_served = 11;
|
||||
TipFeedbackPayload tip_feedback = 12;
|
||||
TipRewardFailedPayload tip_reward_failed = 13;
|
||||
IntegrationTokenExpiredPayload integration_token_expired = 14;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
syntax = "proto3";
|
||||
package oo.events.v1;
|
||||
|
||||
// subject: signals.integration.token_expired
|
||||
message IntegrationTokenExpiredPayload {
|
||||
string user_id = 1;
|
||||
string provider = 2;
|
||||
string detected_at = 3; // ISO 8601
|
||||
}
|
||||
39
packages/shared-types/events/oo/events/v1/signals.proto
Normal file
39
packages/shared-types/events/oo/events/v1/signals.proto
Normal file
@@ -0,0 +1,39 @@
|
||||
syntax = "proto3";
|
||||
package oo.events.v1;
|
||||
|
||||
// subject: signals.task.synced
|
||||
message TaskSyncedPayload {
|
||||
string user_id = 1;
|
||||
string source = 2; // e.g. "todoist"
|
||||
int32 count = 3;
|
||||
string synced_at = 4; // ISO 8601
|
||||
}
|
||||
|
||||
// subject: signals.tip.served
|
||||
message TipServedPayload {
|
||||
string user_id = 1;
|
||||
string tip_id = 2;
|
||||
string policy = 3;
|
||||
string served_at = 4; // ISO 8601
|
||||
}
|
||||
|
||||
// subject: signals.tip.feedback
|
||||
// action: done | dismiss | snooze | helpful | not_helpful
|
||||
message TipFeedbackPayload {
|
||||
string user_id = 1;
|
||||
string tip_id = 2;
|
||||
string action = 3;
|
||||
double reward = 4;
|
||||
optional int64 dwell_ms = 5; // null when no dwell was recorded
|
||||
string created_at = 6; // ISO 8601
|
||||
}
|
||||
|
||||
// subject: signals.tip.reward_failed
|
||||
message TipRewardFailedPayload {
|
||||
string user_id = 1;
|
||||
string tip_id = 2;
|
||||
double reward = 3;
|
||||
int32 attempts = 4;
|
||||
string error = 5;
|
||||
string failed_at = 6; // ISO 8601
|
||||
}
|
||||
Reference in New Issue
Block a user