PR-OUTBOX-PREREQ-PROTO — Avro schemas for identity ticket lifecycle events #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/outbox-prereq-avro-schemas"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Protobuf-side half of
PR-OUTBOX-PREREQper ADR-0002 §3b (im2be-mono). Adds three Avro schemas underavro/identity/ticket/event/v1/for the Kafka outbox topics emitted by identity-service on the Centrifugo opaque-ticket lifecycle.What's in
ticket_minted.avsc—TicketMinted(UUIDv7 event_id, full user/family/tier/parental snapshot, expires_at)ticket_expired.avsc—TicketExpired(UUIDv3-deterministic event_id fromticket_id + expires_at_iso8601so sweeper double-fires collapse at consumer dedup per ADR-0014 D-4, plusconsumedflag)ticket_revoked.avsc—TicketRevoked(UUIDv7 event_id,TicketRevokeReasonenum with UNSPECIFIED fail-closed default)avro/README.md— layout + naming + backward-compat rules (Apicurio BACKWARD mode per ADR-0011 §C)All three records carry the canonical Kafka envelope (
event_id+event_time+schema_version+trace_id).What's NOT in
TicketExpirySweeper— lands asPR-OUTBOX-1inim2be-identity-service..avscfiles — TODO follow-up; today's validation is the manualpython3 -c json.loadsyntax check + the implicit Apicurio-register-on-startup gate in PR-OUTBOX-1.PR-OUTBOX-{family,admin,chatbot}later in Wave A/B/C.Cross-references
im2be-mono— the canonical specification for these schemas (drafted at meta-repo2ec2c3b)processed_kafka_eventscompound-key dedup.planning/26-stage-b-outbox-parity.md§10 — Wave A execution planTest plan
.avscfiles parse as valid JSON (python3 -c 'json.load(open(...))')avro-toolsround-trip (deferred to local validation by the reviewer)avro-maven-plugin(validated in PR-OUTBOX-1 when identity-service consumes these)🤖 Generated with Claude Code
Adds avro/identity/ticket/event/v1/{ticket_minted,ticket_expired,ticket_revoked}.avsc matching ADR-0002 §3b.3-§3b.5 in im2be-mono. These are the on-wire payloads for identity-service's three Kafka outbox topics: - identity.ticket.minted.v1 (TicketMinted, UUIDv7 event_id) - identity.ticket.expired.v1 (TicketExpired, UUIDv3-deterministic event_id from ticket_id + expires_at, so sweeper double-emits collapse at consumer dedup per ADR-0014 D-4) - identity.ticket.revoked.v1 (TicketRevoked, UUIDv7 event_id, UNSPECIFIED reason = fail-closed) All three records carry the canonical Kafka event envelope (event_id + event_time + schema_version + trace_id) plus per-event payload. Java namespace com.aim2be.identity.ticket.event mirrors the planned avro-maven-plugin codegen target in identity-service. New avro/README.md documents: - Layout convention (proto vs avro coexistence in the same module) - Naming (namespace, topic, subject, event_id derivation) - Backward-compatibility rules (Apicurio BACKWARD mode per ADR-0011 §C) - Manual validation step (no Avro-specific CI workflow yet — TODO follow-up) Sticky-partitioned by user_id across all three topics so a single consumer processes all lifecycle transitions for one user in order. Cross-refs: - ADR-0002 §3b in im2be-mono (this is the protobuf-side half of PR-OUTBOX-PREREQ) - ADR-0011 §B (Apicurio registry; BACKWARD compatibility mode) - ADR-0014 D-3 (Avro as on-wire encoding) + D-4 (processed_kafka_events compound dedup) - .planning/26-stage-b-outbox-parity.md §10 (PR-OUTBOX-PREREQ scope) Gates PR-OUTBOX-1 (identity-service producer code + TicketExpirySweeper).Show previous round
hib-pr-reviewer review — PR #3 (affinity-intelligence-rework/im2be-protobuf)
Round 1 — head
689ea28a7f68, basemain, triggeropenedTL;DR: NEEDS_WORK — kept 5 findings (1 agreed minor, 1 A-major, 2 A/B minor, 1 info-defer); dropped B's blocking claim on revoke_reason because its core premise — that Avro enum type-level
defaultis silently ignored — is factually incorrect per the Apache Avro specification.Summary
Arbitration — PR #3
im2be-protobufAvro schemasMemora: No prior runs found for this PR. First arbitration. Run summary persisted (memory ID 278).
Verification performed
idl2schematawrong subcommandidl2schematapresent, both A+B agreeSubscriptionTierenum type missingdefault"default"key inside the enum type objectschema_versiondoc contradicts READMEsubscription_tierfield missing field-level default"default"at the field levelrevoke_reasondefault silently ignored"default"attribute used during schema resolution when a reader encounters an unknown symbol. It is not silently ignored. The"default": "UNSPECIFIED"at L45 insideTicketRevokeReasonis correctly placed and functions as intended. B's conclusion thatrevoke_reasonhas "no actual default" and "breaks the fail-closed contract" does not follow. → DROPPED.Reconciliation outcome
defaultsilently ignored) is incorrect per Avro spec; consequence described (fail-closed contract broken) is unverified and does not follow from the actual codeBlast Radius
All changes are net-new files in a newly created
avro/subdirectory; no existing proto files or service code are modified. Blast is bounded to future consumers of the three new Kafka topics defined by these schemas. The schema evolution gaps (missing enum-type default, missing field-level defaults) would only materialize when a new symbol or field is added in a future v1 revision.BLAST_SCORE: 3/10
CI status (head
689ea28a7f68)Overall: ✗ failure
3 checks: 3 pending
Findings (5)
[MINOR] Wrong avro-tools subcommand:
idl2schematadoes not accept.avscinputavro/README.md:54
Line 54 recommends:
idl2schemataparses Avro IDL (.avdl) textual definitions, not JSON schema (.avsc). Passing a.avscfile throwsorg.apache.avro.AvroRuntimeException: Not an avro IDL fileimmediately — no validation occurs.Replace with the
compile schemasubcommand, which parses and validates JSON schema structure:Run once per file (or glob all three). This is the standard avro-tools path for validating
.avscfiles locally and will surface parse errors, unknown logicalTypes, and bad field structure.[MAJOR]
SubscriptionTierenum type has no"default"— old consumers will hard-fail on any new tier symbolavro/identity/ticket/event/v1/ticket_minted.avsc:40
The
subscription_tierfield's inline enum type (lines 41-46) has no"default"attribute:Per the Avro specification (§Enums), the enum-type
"default"is the fallback used during schema resolution when a reader (old schema) encounters a symbol written by a newer producer that it does not recognise. Without it, adding any new tier symbol (e.g.ENTERPRISE) in a future schema version will throwSchemaResolutionExceptionin every consumer that has not yet deployed the updated schema — a silent compatibility cliff that contradicts the stated BACKWARD compat stance in README §Backward-compatibility rules.Note the contrast with
ticket_revoked.avsc:TicketRevokeReasoncorrectly has"default": "UNSPECIFIED"inside its enum type object and this is exactly right per the Avro spec. Apply the identical pattern here:"UNSPECIFIED"must be insymbols(it is) and should be the first symbol (it is) — the constraint is satisfied.[MINOR]
schema_versiondoc says "every breaking change" — directly contradicts README §Naming conventionsavro/identity/ticket/event/v1/ticket_minted.avsc:21
Line 21 doc reads:
The README (line 34) says the opposite:
schema_versionis "bumped on backward-compatible additions". Breaking changes require creating a newv2/directory (README line 24); aschema_versionbump is for safe additions within the same major version. The inline doc inverts the rule — any implementer reading only the.avscwill do the wrong thing.Change line 21 to:
Note:
ticket_expired.avscandticket_revoked.avscomit theschema_versiondoc entirely — adding a consistent doc string there would also help.[MINOR]
subscription_tierfield has no field-level"default"— forecloses the remove-with-default evolution pathavro/identity/ticket/event/v1/ticket_minted.avsc:40
The
subscription_tierfield (line 40) declares an inline enum type but has no"default"at the field level (i.e. as a sibling of"name"and"type", outside the type object). This is a separate concern from the missing enum-typedefault(see preceding finding).Per README §Backward-compatibility rules line 43: "✅ Removing a field that had a default." A field with no field-level default can never be safely dropped in a BACKWARD-compatible schema change.
"UNSPECIFIED"is already the first symbol (Avro requires the field default to be one of the declared symbols), so adding it now has zero impact on existing producers or consumers:This also keeps all future dual-write transition options open for re-typing the field.
[INFO] No CI Avro lint — schema breakage undetectable until Apicurio registration at service startup
avro/README.md:50
README line 50 acknowledges: "There is no Avro-specific CI lint workflow yet (TODO follow-up)." Until a CI job validates
.avscsyntax on every PR, a malformed schema will only surface whenPR-OUTBOX-1hits the Apicurio registration call at service startup. Consider filing a follow-up issue to addavro-tools compile schemaas a pre-merge check. (Out of scope for this PR — tracked as a follow-up tech-debt item.)Verdict
NEEDS_WORK
hib-pr-reviewer • round 1 • 5 findings (1M/3m/1i) • 2026-05-26T09:08:08.024Z → 2026-05-26T09:13:05.507Z • posted-as: pr-reviewer-bot
Show previous round
hib-pr-reviewer review — PR #3 (affinity-intelligence-rework/im2be-protobuf)
Round 2 — head
76a094504a6a, basemain, triggersynchronizeTL;DR: CONDITIONAL_APPROVE — kept 1 agreed finding (revoke_reason missing field-level default) and 2 verified unique-to-B findings (schema_version missing doc in ticket_expired.avsc and ticket_revoked.avsc); all 3 are minor, no blockers.
Summary
Recalled 0 Memora memories for this PR (no prior run history stored). Verified all 4 candidate findings against HEAD files at the checkout path.
Agreed finding (A + B):
ticket_revoked.avsc:33—revoke_reasonfield carries a type-level"default": "UNSPECIFIED"inside theTicketRevokeReasonenum (line 45) but has no field-level"default"on the field object itself (lines 33–49 read and confirmed). This directly parallels the R1-4 fix applied tosubscription_tierinticket_minted.avscand forecloses the remove-with-default evolution path. KEPT.Unique-to-B finding 1 (verified):
ticket_expired.avsc:18—schema_versionat lines 18–21 reads{"name":"schema_version","type":"int","default":1}with no"doc". The corresponding field inticket_minted.avsccarries the evolution-guidance doc added in round 1. KEPT.Unique-to-B finding 2 (verified):
ticket_revoked.avsc:18— identical gap at lines 18–21. KEPT.Memora persisted as new record (id=279, review_count=2) per the create-new fallback (no prior run memory existed despite round=2). 3 minor findings kept; 0 dropped.
Blast Radius
The diff adds four new files (3 Avro schemas + 1 README) with no modifications to existing files. The schemas are new Kafka event contracts not yet consumed by any production service — blast radius is bounded to downstream codegen and the as-yet-unmerged outbox publisher PR (PR-OUTBOX-1). The missing field-level default on
revoke_reasonis the highest-impact gap: it would silently foreclose a future backward-compatible evolution step.BLAST_SCORE: 3/10
Risk Indicators
TicketRevokeReason.SECURITY_INCIDENT,TicketRevokeReason.USER_SUSPENDED,TicketRevokeReason.ADMIN_FORCE_DISCONNECTCI status (head
76a094504a6a)Overall: ✗ failure
3 checks: 3 pending
Findings (3)
[MINOR]
revoke_reasonenum field has type-level default but no field-level"default": "UNSPECIFIED"— inconsistent withsubscription_tierpattern, forecloses evolutionavro/identity/ticket/event/v1/ticket_revoked.avsc:33
Verified at HEAD lines 33–49: the
revoke_reasonfield object has no"default"sibling to"name". TheTicketRevokeReasonenum type embedded at lines 34–47 does carry"default": "UNSPECIFIED"(line 45), but that is the type-level (unknown-symbol resolution) default — a distinct Avro mechanism from the field-level default (field-resolution when a writer omits the field entirely).Consequences without the field-level default:
revoke_reasoncannot be cleanly removed in a backward-compatible way under the README's own rule ("✅ Removing a field that had a default"); a future v1 revision would have to introduce the field-level default as an intermediate step anyway.Field revoke_reason type:ENUM pos:2 not foundrather than substitutingUNSPECIFIED.subscription_tierinticket_minted.avsc(lines 40–49), which this PR explicitly fixed in round 1 to carry both type-level and field-level defaults with an explanatorydoc.Fix — add
"default": "UNSPECIFIED"as a sibling of"doc"on the field object and update"doc"to match thesubscription_tierrationale pattern:[MINOR]
schema_versionmissing"doc"— evolution-guidance clarification from round-1 fix not propagated fromticket_minted.avscavro/identity/ticket/event/v1/ticket_expired.avsc:18
Verified at HEAD lines 18–21: the
schema_versionfield inticket_expired.avscis defined as{"name":"schema_version","type":"int","default":1}with no"doc".The round-1 fix to
ticket_minted.avscadded the following"doc"to resolve the ambiguity about when to bumpschema_versionvs. create a newv2/directory:Without this clarification in
ticket_expired.avsc, a maintainer reading only that file has no guidance and faces the exact ambiguity that prompted finding R1-3.Fix: add the identical
"doc"text toschema_versioninticket_expired.avsc.[MINOR]
schema_versionmissing"doc"— same evolution-guidance gap asticket_expired.avscavro/identity/ticket/event/v1/ticket_revoked.avsc:18
Verified at HEAD lines 18–21: the
schema_versionfield inticket_revoked.avscis defined as{"name":"schema_version","type":"int","default":1}with no"doc"— identical gap toticket_expired.avsc.The fix applied to
ticket_minted.avscin round 1 should have been propagated to all three schema files. It was not.Fix: add the same
"doc"text:Verdict
CONDITIONAL_APPROVE
hib-pr-reviewer • round 2 • 3 findings (3m) • 2026-05-26T09:18:31.431Z → 2026-05-26T09:20:11.375Z • posted-as: pr-reviewer-bot
Show previous round
hib-pr-reviewer review — PR #3 (affinity-intelligence-rework/im2be-protobuf)
Round 3 — head
91e2ed169b9a, basemain, triggersynchronizeTL;DR: CONDITIONAL_APPROVE — kept 1 verified unique-to-A minor finding (consumed boolean missing default:false in ticket_expired.avsc); Reviewer B output was truncated (0 findings); no blocking issues.
Summary
Round 3 Arbitration
Reviewer B produced 0 usable findings (output truncated mid-JSON, parse-fallback).
Reviewer A raised 1 unique-to-A minor finding:
consumedboolean inticket_expired.avscmissing"default": false.Verification performed (Read tool,
/var/lib/pr-reviewer/repos/affinity-intelligence-rework/im2be-protobuf/767/avro/identity/ticket/event/v1/ticket_expired.avsc):consumedfield declares"type": "boolean"with no"default"key — finding holds exactly as cited.ticket_minted.avsclines 78–80 confirmed: structurally identicalis_adminboolean carries"default": false— asymmetry verified.✅ Removing a field that had a defaultas the safe Avro evolution path; absence of a default onconsumedforecloses that path without a major-version bump.Kept 1 finding (unique-to-A, verified). Dropped 0. Reviewer B contributed nothing verifiable due to truncation.
Note: Memora persistence attempted but failed due to tag allowlist restrictions on the MCP server; run history could not be stored.
Blast Radius
This PR adds new Avro schema files and a README under avro/identity/ticket/event/v1/. The blast radius is limited to the three new event schemas (ticket_minted, ticket_expired, ticket_revoked) and their downstream Kafka consumers. No existing proto files or service contracts are modified; the schemas are not yet registered in production (registration happens in PR-OUTBOX-1).
BLAST_SCORE: 3/10
CI status (head
91e2ed169b9a)Overall: ✗ failure
3 checks: 3 pending
Findings (1)
[MINOR]
consumedboolean missing"default": false— inconsistent withis_adminpattern and forecloses README's two-step removal pathavro/identity/ticket/event/v1/ticket_expired.avsc:39
At lines 38–42, the
consumedfield is declared as"type": "boolean"with no"default"key:Compare
ticket_minted.avsclines 77–81 where the structurally identical boolean fieldis_admincarries"default": false.The README's backward-compatibility rules (new in this very PR) list
✅ Removing a field that had a defaultas the safe evolution path. Without a default onconsumed, a future schema that drops this field cannot be registered as a backward-compatible v1.x change — it would require a major-version bump (v1/→v2/). Thedocitself states "False = ticket was minted but never used", confirmingfalseis the correct missing-data sentinel.Fix: add
"default": falsebetween"type"and"doc":Verdict
CONDITIONAL_APPROVE
hib-pr-reviewer • round 3 • 1 finding (1m) • 2026-05-26T09:24:54.399Z → 2026-05-26T09:26:18.943Z • posted-as: pr-reviewer-bot
Show previous round
hib-pr-reviewer review — PR #3 (affinity-intelligence-rework/im2be-protobuf)
Round 4 — head
b2f2b369f7f2, basemain, triggersynchronizeTL;DR: CONDITIONAL_APPROVE — all 9 prior-round findings confirmed resolved; kept 1 verified minor unique-to-B (README validation command omits two of three schema files).
Summary
Arbiter Reconciliation — Round 4
Prior findings (R1–R3, 9 total): Both reviewers independently confirm all resolved. Not re-raised.
New findings this round:
compile schemacommand names onlyticket_minted.avsc, leavingticket_expired.avscandticket_revoked.avscoutside the manual-validation path.Verification (unique-to-B finding): Read
/var/lib/pr-reviewer/repos/affinity-intelligence-rework/im2be-protobuf/768/avro/README.mdline 58. Confirmed exact text:Only one of three
.avscfiles is named.ticket_expired.avscandticket_revoked.avscare silently omitted. Finding kept.Memora persistence:
memory_createandmemory_absorbrejected due to tag-allowlist constraints in the Memora instance; persistence attempted but unavailable this session.Outcome: 1 verified minor finding kept; verdict CONDITIONAL_APPROVE.
Blast Radius
The diff adds three new Avro schema files and a README under
avro/. No existing files are modified. The schema files are not yet consumed by any service in this repo — the outbox publisher lands in a separate PR (PR-OUTBOX-1). Blast radius is low and contained to the newavro/subtree.BLAST_SCORE: 2/10
CI status (head
b2f2b369f7f2)Overall: ✗ failure
3 checks: 3 pending
Related PRs
Findings (1)
[MINOR] Manual-validation command covers only
ticket_minted.avsc— two schemas silently skippedavro/README.md:58
The
compile schemainvocation at line 58 is:A developer following this verbatim will never lint
ticket_expired.avscorticket_revoked.avsc.avro-tools compile schemaaccepts a directory as its schema argument and will process all.avscfiles within it; change the invocation to:Alternatively list all three files explicitly. This gap is especially consequential while the CI follow-up (
PR-OUTBOX-PREREQ-PROTO-FOLLOWUP-avro-ci) remains pending and the manual command is the only validation path.Verdict
CONDITIONAL_APPROVE
hib-pr-reviewer • round 4 • 1 finding (1m) • 2026-05-26T09:28:52.005Z → 2026-05-26T09:30:07.399Z • posted-as: pr-reviewer-bot
hib-pr-reviewer review — PR #3 (affinity-intelligence-rework/im2be-protobuf)
Round 5 — head
eec0073d6b81, basemain, triggersynchronizeTL;DR: NO_NEW_FINDINGS — No new findings this round.
Summary
[quorum-converged] A=0 = B=0. Round 5 — All 10 prior findings resolved; no new actionable findings.
Full file reads confirm every open concern from rounds 1–4 has been addressed:
idl2schematasubcommandDO NOT use idl2schematacomment + correctcompile schemausageSubscriptionTierno type-leveldefault"default": "UNSPECIFIED"on the enum type at line 45schema_versiondoc contradicted READMEsubscription_tierno field-leveldefault"default": "UNSPECIFIED"at line 48PR-OUTBOX-PREREQ-PROTO-FOLLOWUP-avro-ciinim2be-monorevoke_reasonno field-leveldefault"default": "UNSPECIFIED"atticket_revoked.avscline 49ticket_expired.avsc schema_versionmissingdocticket_revoked.avsc schema_versionmissingdocconsumedboolean missingdefault: false"default": falseatticket_expired.avscline 41find avro -name '*.avsc' -print0 | xargs -0 -n1loop at README lines 63–64All three
.avscfiles are internally consistent: every evolvable field carries adefault, both type-level and field-level defaults are present on inline enum types (SubscriptionTier,TicketRevokeReason),schema_versiondocs are uniform across all three schemas, and the README lint command now covers every.avscfile via glob. Schemas are ready for registration.CI status (head
eec0073d6b81)Overall: ✗ failure
3 checks: 3 pending
Findings
No new findings this round.
Quorum converged on empty findings (A + B both returned 0).
Verdict
NO_NEW_FINDINGS
hib-pr-reviewer • round 5 • 0 findings • 2026-05-26T09:30:41.583Z → 2026-05-26T09:33:10.659Z • posted-as: pr-reviewer-bot • [bookkeeping fallback]