بنقرة واحدة
schema-compatibility-review
Internal helper. Load only when explicitly named by another skill or agent.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Internal helper. Load only when explicitly named by another skill or agent.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
This skill should be used when the user asks to "babysit a PR", "babysit my pull request", "monitor my PR", "watch my pull request", "keep my PR green", "fix PR build failures automatically", "handle PR review comments", or wants autonomous Azure DevOps PR monitoring that fixes build breaks, test failures, code coverage gaps, and review comments on a polling loop.
Internal helper. Load only when explicitly named by another skill or agent.
Internal helper. Load only when explicitly named by another skill or agent.
Publish local changes as an Azure DevOps pull request — analyzes commits, creates or links a work item (bug, task, or user story), pushes the branch, composes a PR description, and optionally tends to reviewer feedback and build failures until the PR is merged.
Internal helper. Load only when explicitly named by another skill or agent.
Internal helper. Load only when explicitly named by another skill or agent.
| name | schema-compatibility-review |
| description | Internal helper. Load only when explicitly named by another skill or agent. |
| user-invocable | true |
| disable-model-invocation | false |
| allowed-tools | ["Read","Grep","Glob"] |
This skill is the working methodology for any reviewer (human or agent) checking that schema and wire-contract changes are safe to deploy. It applies to every place where data crosses a version boundary: a write today must be readable by code deployed yesterday and by code deployed tomorrow.
You cannot atomically upgrade a client, a server, a database, and all the persisted data they have already written. There is always a deploy window — sometimes minutes, sometimes weeks, sometimes (for stored data) forever — during which old and new code coexist. Every shape that crosses that window must be readable by both sides.
The damage from a compatibility break is asymmetric:
Once data is written in the new shape, the old shape is gone. You cannot roll back the schema without losing the writes that happened after the migration. That makes schema changes one of the few categories where "we'll fix it forward" is genuinely a one-way door.
Hold a high bar here. Default backward-incompatible schema changes to BLOCKER until an explicit migration plan is in place.
Use this methodology when a PR touches any of the following:
.proto, .thrift, .fbs, .avsc, .bond, or other schema-definition file.[GenerateSerializer] / [Id] (Orleans), [DataContract] /
[DataMember], [JsonPropertyName], [ProtoMember], [BondMember], @JsonProperty, etc.appsettings.*,
settings.json, cache entries).You should also use this skill whenever a reviewer or work item raises a compatibility question — "old clients won't have this field," "this rolls out before the server side," "what about clients still on the previous build?", "is this safe for a rolling deploy?", etc.
Every schema review walks the same five lenses. Most PRs only touch two or three, but check each one.
| Lens | The question |
|---|---|
| 1. Backward compatibility | Can new code read data written by old code? |
| 2. Forward compatibility | Can old code safely handle data written by new code? |
| 3. Rollout sequencing | If client and server land in different deploys, who moves first? |
| 4. Public-surface stickiness | Is this shape consumed by something we can't redeploy in lockstep? |
| 5. Serialize/deserialize symmetry | Do the sender and receiver agree on the type shape? |
The catalog below is organized by change pattern, but each pattern maps to one or more of these five lenses. If you find a finding that doesn't fit any lens, it probably belongs to a different review skill.
Each pattern includes: detection signals, which lens it violates, severity guidance, recommendation, and explicit "do NOT flag" rules.
A field that previously existed in the schema has been deleted, or had its serialized name or wire identifier changed.
Detection signals:
[GenerateSerializer], [DataContract], [JsonPropertyName], or
[ProtoMember]-annotated class.[JsonPropertyName("foo")],
[ProtoMember(3, Name = "foo")]) was also changed.reserved keyword not added next to the removed field number.[Id(N)] removed, where N is not added to [GenerateSerializer]'s
reserved list.Lens: Backward compatibility (new code drops data that old code wrote) and forward compatibility (old code may still try to write this field).
Why it matters:
[Id] number) is the same
as deleting and re-adding — the old data is unreadable under the new name.Severity: BLOCKER by default for any shape that is persisted, sent over a network, or written to a queue. HIGH if the type is purely in-process but crosses a deployable boundary. LOW only when you can prove no instance of the old shape exists anywhere — typically a brand-new type added and removed in the same PR.
Recommendation: Don't remove or rename. Instead:
Do NOT flag:
[GenerateSerializer],
[DataContract], [JsonPropertyName], no JsonSerializer.Serialize call site, not a DTO).reserved placeholder or sentinel that was clearly never meaningful data.A new field that lacks a sensible default, is marked non-nullable, or is enforced as required by the serializer or database.
Detection signals:
JsonSerializerOptions with RespectRequiredConstructorParameters = true,
[JsonRequired], proto3 optional keyword absent, Bond required-modifier, etc.).NOT NULL column without a default and without a backfill step.[GenerateSerializer] type with no default value.[FromBody] and
rejects requests missing the field.Lens: Backward compatibility (old writers don't include the field; new readers reject the payload).
Why it matters:
null/default and propagate it silently into business logic that doesn't
expect it.Severity: BLOCKER when persisted data or in-flight messages may lack the field. HIGH for new wire contracts where old clients exist. MEDIUM when the field is added to a brand-new type only created by the new code.
Recommendation: Make the field optional (?, Option<T>, proto3 optional, nullable
column) with a sensible default. If the value is genuinely required for new logic, treat
"absent" as a domain-level signal — fall back to a default or compute it from other fields —
and only enforce required-ness at the boundary that owns the new behavior, not at the
deserializer.
For DB migrations: add the column nullable, deploy, backfill in a second migration, then optionally tighten to NOT NULL only after every row is filled.
Do NOT flag:
default(T) on missing properties, where the consuming
code already handles default).The field name stayed the same, but its type, units, encoding, or meaning changed.
Detection signals:
int → long, string → enum, decimal → double, DateTime → DateTimeOffset.timeoutMs now interpreted
as seconds; userId now stores a tenant-qualified ID).byte → int) — wire-incompatible in binary
formats.Lens: Backward compatibility AND forward compatibility — both sides will misinterpret data that crosses the boundary.
Why it matters:
long → int) silently truncates large values.int → long) may break strict deserializers and overflow downstream
consumers that still expect int.ms → s, bytes → KB) cause off-by-1000 bugs that no test catches because
both sides individually look correct.Severity: BLOCKER when the change crosses a persistence or network boundary. HIGH for purely in-process changes if the type is shared across deployables.
Recommendation: Don't change types in place. Add a new field with the new type, populate both during the transition, switch readers to the new field, then deprecate the old. For unit or semantic changes, rename the field — the rename forces every reader to be updated consciously, which is the point.
Do NOT flag:
int32 to sint32 is
wire-different but the same source change usually isn't done; check the wire format).An enum value was deleted, renamed, or had its underlying integer changed.
Detection signals:
.cs / .proto file where the enum is serialized.[JsonStringEnumConverter] enum where a value's display name changed.Lens: Backward compatibility — persisted records carry the old integer or string. The new code either rejects them or silently maps them to a different value.
Why it matters:
3 previously meant "Cancelled" and now means "Refunded".
Every old row is silently mis-categorized.Severity: BLOCKER for any enum that is persisted or transmitted. HIGH for purely in-process enums that cross a deployable boundary.
Recommendation: Don't remove enum values. Mark them [Obsolete] and stop producing them
in a release after all consumers have updated their handling. Don't renumber — for protobuf,
reserved the old number permanently. For string enums, treat the old name as an alias rather
than renaming.
Do NOT flag:
switch statement throws on unknown values, the new value
becomes a forward-compat break.A constraint became stricter: nullable became required, an optional column became NOT NULL, a min/max range narrowed, a regex pattern got tighter, a unique index was added.
Detection signals:
int? Foo → int Foo.[Required] added to an existing property.ALTER COLUMN ... SET NOT NULL in a migration.[StringLength(100)] → [StringLength(50)],
[Range(0, 100)] → [Range(10, 50)]).Lens: Backward compatibility — existing data may violate the new constraint.
Why it matters:
Severity: BLOCKER if the constraint applies to persisted data and existing rows might violate it. HIGH for API request validation tightening (old clients silently break). LOW when the constraint is on a brand-new field added in the same PR.
Recommendation: Audit existing data first. Either:
Do NOT flag:
The client speaks a new dialect — sends a new field, expects a new response shape, calls a new endpoint — that the deployed server doesn't yet support, with no flag or version gate to hold the new behavior until the server is ready.
Detection signals:
Lens: Rollout sequencing.
Why it matters:
Severity: BLOCKER when the rollout would produce a user-visible failure (client errors, 500s, broken pages). HIGH when there is a graceful degradation but no flag (the new behavior quietly doesn't work for a window). MEDIUM if both sides are internal services with coordinated deploy and the window is short.
Recommendation: One of:
if (featureEnabled)
so that until the flag flips, the client speaks the old dialect. The flag flips only after
the server-side change is fully deployed./capabilities or version endpoint and
switch on the result.For service-to-service: the consumer must tolerate the producer running either the old or new build for the entire duration of the rolling deploy.
Do NOT flag:
The change touches a shape consumed by something outside the team's control: an external SDK, a customer integration, a third-party webhook, persisted data with a long retention policy.
Detection signals:
api/, public/, sdk/, contracts/, dto/, schema/, or otherwise
externally-named module.Lens: Public-surface stickiness.
Why it matters:
userId: string, you cannot change it to userId: int without breaking them, period.Severity: BLOCKER for any back-incompat change to a public/external surface, unless the
PR explicitly documents a versioning strategy (/v2/users, breaking change in a major SDK
version with announcement).
Recommendation: Treat public surface as append-only or versioned:
/v2, a new SDK major) and keep the
old one alive for a deprecation window. Document the deprecation policy.Do NOT flag:
api/ — check the
actual access modifier and the public-symbol list of the package.The sender and receiver of a payload use different concrete types for the "same" data, and those types can drift independently. A renamed field on one side silently drops data on the other.
Detection signals:
ClientUserRequest while the producer serialized
ServerUserResponse — and the two types are defined in separate files with no shared base
type or contract test.EventPayloadDto whose fields
duplicate (but don't share) the producer's type.dynamic, JObject, or JsonElement deserializer extracting fields by string name — if
the producer ever renames the field, the consumer goes silent.[GenerateSerializer] types in a project's Models/ and Contracts/ directories with
near-identical field sets and no test that round-trips between them.(string)dict["userId"]) — drift is undetectable until production.Lens: Serialize/deserialize symmetry.
Why it matters:
Severity: HIGH when the mismatch is on a critical path or a frequently-changed type; MEDIUM otherwise. BLOCKER when the PR is introducing the duplication (now is the cheapest moment to fix it).
Recommendation: Use one of:
.proto / .avsc / OpenAPI spec and
generate the types for both sides. Schema is the source of truth; the generated code can't
drift.If the PR is touching an existing mismatched pair, flag the underlying issue even if the specific change is fine. The next change will be the one that breaks.
Do NOT flag:
A migration changes the shape of persisted data in a way that can't be rolled back cleanly, or that requires data to exist before it can succeed.
Detection signals:
Down / rollback script when the project's convention is
to provide one.Lens: Backward compatibility AND rollout sequencing AND public-surface stickiness — DB migrations hit all three because the database outlives every individual deploy.
Why it matters:
Severity: BLOCKER by default. The blast radius of a bad migration is the whole product; even a working migration that hasn't been thought through for rolling deploys can break production.
Recommendation: Apply the expand–migrate–contract pattern:
Each step is a separate deploy. Each step is independently reversible.
Do NOT flag:
When reviewing a PR, walk the diff with these questions in order:
For each finding, populate the agent's output template with:
The patterns above are universal, but each schema technology has fiddly per-tech rules — proto
field numbers, Orleans [Id] slots, EF migration conventions, JSON converter behaviors. Load
the relevant reference only when the PR involves that technology:
references/protobuf.md (field numbers, reserved, wire-compatible
type changes, well-known types).references/json-dto.md (serializer mode flags, naming
policies, additionalProperties, polymorphic deserialization).[GenerateSerializer] types — see references/orleans.md
([Id] discipline, [Alias], surrogate types, grain interface versioning).references/db-migrations.md (expand-
migrate-contract templates, online-DDL caveats, default-value pitfalls).references/binary-formats.md (schema
evolution rules per format, default handling, union types).| Anchor source | Confidence | Posture |
|---|---|---|
| Work item that explicitly addresses compatibility / migration | HIGH | Confidently flag deviations from the stated plan |
| PR description names rollout order / a flag / a migration plan | HIGH | Same |
| Schema change with no compatibility discussion | MEDIUM | Flag confidently; emit [QUESTION] if you can't tell what's persisted vs. ephemeral |
| Ambiguous (client and server in same PR, no mention of how they roll out) | LOW | Emit [QUESTION] on rollout pattern 6, flag confidently on the others |
State the anchor and confidence level in the summary block.
OrderId is removed
from OrderCreatedEvent (Models/Events/OrderCreatedEvent.cs:42). This event is published to
Service Bus topic orders and consumed by the BillingService and NotificationService,
which deserialize into their own copies of the type and read OrderId. After this deploy, in-
flight messages written by the old producer will deserialize into a null OrderId on the
consumer side and silently fail." is useful.[QUESTION]
and request clarification rather than waving the change through.