File size: 52,456 Bytes
5cbafe2 cbe10c3 b7f158e cbe10c3 b7f158e cbe10c3 b7f158e cbe10c3 ebe72d3 b7f158e cbe10c3 ebe72d3 b7f158e 5cbafe2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Protobuf Wire compatibility Check — check proto changes before rollout</title>
<meta name="description" content="This free browser tool flags wire-incompatible .proto edits, including reused or unreserved field numbers, enum zero drift, and JSON name collisions.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/proto-wire-break-check"></head>
<body>
<div class="wrap">
<h1>Protobuf Wire compatibility Check — check proto changes before rollout</h1>
<p class="lede">Flags wire-incompatible edits in .proto files - reused or unreserved field numbers, enum zero drift, JSON name collisions - before a gRPC rollout silently corrupts a field. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">syntax = "proto3";
// The package line was dropped in a refactor.
message Invoice {
reserved 4;
reserved "legacy_total";
string invoice_id = 1;
int64 amount_cents = 2;
string currency = 2;
int64 legacy_total = 4;
string invoiceId = 5;
string note = 9;
required string payer = 10;
string region = 11 [default = "eu"];
repeated int32 shard_ids = 12 [packed = false];
string currency = 13;
repeated string tags = 19500;
}
enum InvoiceState {
INVOICE_STATE_DRAFT = 1;
INVOICE_STATE_SENT = 2;
INVOICE_STATE_PAID = 2;
INVOICE_STATE_VOID = 7;
}</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<p id="getfull" style="margin:14px 0 0"><a href="https://getreadystack.com/api/buy/cl/polar_cl_qJoSHN0MM1nBgb9WtR2jW9kBTNvjLxyfKL1hP0NGwfK" style="display:inline-block;font:600 15px system-ui;padding:11px 18px;border-radius:8px;background:#1a1512;color:#fff;text-decoration:none">Audit every .proto in the repo and write the report — $29</a><br>
<small style="color:#7a6e65">$29 once · one licence key per person or team seat · A contract backend engineer runs $60+/hour on the public marketplaces.</small></p>
<div id="paid"><strong>Free here:</strong> Reads the .proto file you have open and names every wire-breaking field, the line it is on, and the edit that keeps old readers working.. <strong>In the editor:</strong> Audits every .proto in the workspace at once and writes a PROTO-WIRE-AUDIT.md plus audit.json you can commit, attach to the pull request, or fail a build on. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var PWB_RULES = [
{"id": "field_number_reuse", "sev": "error", "title": "Two fields share one field number", "fix": "Give the new field the next unused number and never move an existing one."},
{"id": "reserved_number_reused", "sev": "error", "title": "A reserved field number was taken back", "fix": "Leave reserved numbers reserved forever and allocate a fresh number."},
{"id": "reserved_name_reused", "sev": "error", "title": "A reserved field name was taken back", "fix": "Pick a new name; reserved names are still in old JSON payloads and generated accessors."},
{"id": "internal_range_19000", "sev": "error", "title": "Field number inside the protobuf-internal range", "fix": "Use a number outside 19000-19999; protoc refuses that range."},
{"id": "field_number_over_max", "sev": "error", "title": "Field number outside the legal range", "fix": "Field numbers run from 1 to 536870911."},
{"id": "unreserved_field_gap", "sev": "warn", "title": "A hole in the field numbers is not reserved", "fix": "Add a reserved statement for the deleted numbers so nobody recycles the tag."},
{"id": "dup_field_name", "sev": "error", "title": "Two fields share one name", "fix": "Rename one of them; protoc rejects duplicate field names in a message."},
{"id": "json_name_collision", "sev": "error", "title": "Two fields map to the same JSON name", "fix": "Rename one field or pin an explicit json_name so JSON and proto agree."},
{"id": "enum_first_not_zero", "sev": "error", "title": "The first proto3 enum value is not zero", "fix": "Add a ..._UNSPECIFIED = 0 value as the first entry."},
{"id": "enum_dup_number_no_alias", "sev": "error", "title": "Two enum values share one number without allow_alias", "fix": "Give the new value its own number, or set option allow_alias = true on purpose."},
{"id": "enum_unreserved_gap", "sev": "warn", "title": "A hole in the enum numbers is not reserved", "fix": "Reserve the removed enum numbers so they are never reissued with a new meaning."},
{"id": "missing_syntax", "sev": "error", "title": "No syntax declaration", "fix": "Put syntax = \"proto3\"; on the first line; without it protoc silently compiles the file as proto2."},
{"id": "missing_package", "sev": "warn", "title": "No package declaration", "fix": "Declare a versioned package so two linked files cannot collide in the global namespace."},
{"id": "proto2_required", "sev": "error", "title": "A required field", "fix": "Use a plain or optional field; a required field can never be removed without breaking every old reader."},
{"id": "proto3_default_value", "sev": "error", "title": "A proto2 default on a proto3 field", "fix": "Delete the default option; proto3 has no custom defaults, so apply it in code."},
{"id": "presence_sensitive_no_optional", "sev": "warn", "title": "Unset and zero cannot be told apart", "fix": "Mark the field optional so the reader can see the difference between absent and zero."},
{"id": "packed_explicit_override", "sev": "warn", "title": "Packing turned off on a repeated scalar", "fix": "Drop packed = false; it changes the bytes on the wire against the proto3 default."}
]
;</script>
<script>// Proto Wire Break Check - one brain, used by the VS Code extension and by the free web page.
var RULES = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.PWB_RULES;
var PWB_BY_ID = {};
for (var pwb_i = 0; pwb_i < RULES.length; pwb_i++) { PWB_BY_ID[RULES[pwb_i].id] = RULES[pwb_i]; }
var PWB_SCALAR = { double: 1, float: 1, int32: 1, int64: 1, uint32: 1, uint64: 1, sint32: 1, sint64: 1,
fixed32: 1, fixed64: 1, sfixed32: 1, sfixed64: 1, bool: 1, string: 1, bytes: 1 };
// Names where "absent" and "zero/false/empty" mean different things to the reader.
var PWB_PRESENCE_HINT = /(^is_|^has_|^id$|_id$|_enabled$|_count$|_amount$|_cents$|_price$|_total$|_at$|_quantity$|_score$|_percent$)/;
var PWB_MAX_FIELD = 536870911;
var PWB_GAP_MAX = 16; // a hole wider than this is a deliberate block allocation, not a deleted field
var PWB_FIELD_RE = /^\s*(repeated\s+|optional\s+|required\s+)?(map\s*<[^>]*>|\.?[A-Za-z_][\w.]*)\s+([A-Za-z_]\w*)\s*=\s*(\d+)\s*(\[[^\]]*\])?\s*;/;
var PWB_ENUMV_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(-?\d+)\s*(\[[^\]]*\])?\s*;/;
var PWB_OPEN_RE = /^\s*(message|enum|oneof|service|extend)\b\s*([A-Za-z_]\w*)?/;
function pwbCamel(n) {
return n.replace(/_+([a-zA-Z0-9])/g, function (m, c) { return c.toUpperCase(); }).replace(/_/g, '');
}
function pwbStripComments(raw) {
var out = [], inBlock = false;
for (var i = 0; i < raw.length; i++) {
var s = raw[i];
if (inBlock) {
var e = s.indexOf('*/');
if (e < 0) { out.push(''); continue; }
s = s.slice(e + 2); inBlock = false;
}
var b = s.indexOf('/*');
while (b >= 0) {
var e2 = s.indexOf('*/', b + 2);
if (e2 < 0) { s = s.slice(0, b); inBlock = true; break; }
s = s.slice(0, b) + ' ' + s.slice(e2 + 2);
b = s.indexOf('/*');
}
var c = s.indexOf('//');
if (c >= 0) s = s.slice(0, c);
out.push(s);
}
return out;
}
function pwbBlock(kind, name, line) {
return { kind: kind, name: name || '(anonymous)', line: line,
fields: {}, fnames: {}, jnames: {}, nums: [],
resNums: {}, resRanges: [], resNames: {},
enumVals: [], enumNums: {}, allowAlias: false };
}
function pwbReserved(b, n) {
if (b.resNums[n]) return true;
for (var i = 0; i < b.resRanges.length; i++) {
if (n >= b.resRanges[i][0] && n <= b.resRanges[i][1]) return true;
}
return false;
}
function pwbReadReserved(b, line) {
var m = line.match(/^\s*reserved\s+(.+?);/);
if (!m) return false;
var body = m[1];
var names = body.match(/"([^"]+)"|'([^']+)'/g);
if (names) {
for (var i = 0; i < names.length; i++) { b.resNames[names[i].replace(/^["']|["']$/g, '')] = true; }
}
var parts = body.split(',');
for (var j = 0; j < parts.length; j++) {
var p = parts[j].trim();
var rg = p.match(/^(\d+)\s+to\s+(\d+|max)$/);
if (rg) { b.resRanges.push([parseInt(rg[1], 10), rg[2] === 'max' ? PWB_MAX_FIELD : parseInt(rg[2], 10)]); continue; }
if (/^\d+$/.test(p)) b.resNums[parseInt(p, 10)] = true;
}
return true;
}
function pwbGaps(nums) {
var s = nums.slice().sort(function (a, b) { return a - b; });
var runs = [], i, n;
if (s.length < 2) return runs;
for (i = 0; i < s.length - 1; i++) {
var lo = s[i] + 1, hi = s[i + 1] - 1;
if (hi < lo) continue;
runs.push([lo, hi, s[i + 1]]);
}
return runs;
}
function check(text, opts) {
opts = opts || {};
var lines = pwbStripComments(String(text == null ? '' : text).split(/\r?\n/));
var findings = [];
function add(id, line, msg) {
var r = PWB_BY_ID[id] || {};
findings.push({ check: id, sev: r.sev || 'warn', msg: msg, line: line });
}
var syntax = '', hasPackage = false;
var depth = 0, blocks = {}, pending = null, closed = [];
function nearestMessage() {
for (var d = depth; d >= 1; d--) { if (blocks[d] && blocks[d].kind === 'message') return blocks[d]; }
return null;
}
function inOneof() {
for (var d = depth; d >= 1; d--) {
if (blocks[d] && blocks[d].kind === 'oneof') return true;
if (blocks[d] && blocks[d].kind === 'message') return false;
}
return false;
}
for (var li = 0; li < lines.length; li++) {
var line = lines[li], ln = li + 1;
if (!line.trim()) continue;
var sm = line.match(/^\s*syntax\s*=\s*["']([^"']+)["']/);
if (sm) { syntax = sm[1]; continue; }
if (/^\s*package\s+[\w.]+\s*;/.test(line)) { hasPackage = true; continue; }
var opens = (line.match(/\{/g) || []).length;
var closes = (line.match(/\}/g) || []).length;
if (opens > 0) {
var om = line.match(PWB_OPEN_RE);
if (om) pending = { kind: om[1], name: om[2], line: ln };
}
if (opens === 0 && closes === 0) {
var cur = blocks[depth] || null;
if (cur && pwbReadReserved(cur, line)) continue;
if (cur && cur.kind === 'enum') {
if (/^\s*option\s+allow_alias\s*=\s*true/.test(line)) { cur.allowAlias = true; continue; }
var ev = line.match(PWB_ENUMV_RE);
if (ev) {
var vn = ev[1], vnum = parseInt(ev[2], 10);
if (cur.enumVals.length === 0 && syntax === 'proto3' && vnum !== 0) {
add('enum_first_not_zero', ln, 'Enum ' + cur.name + ' opens with ' + vn + ' = ' + vnum + '. In proto3 the first value must be 0, so every message that leaves this enum unset decodes as ' + vn + ' instead of "unspecified".');
}
if (cur.enumNums[vnum] !== undefined && !cur.allowAlias) {
add('enum_dup_number_no_alias', ln, vn + ' and ' + cur.enumNums[vnum].name + ' (line ' + cur.enumNums[vnum].line + ') both use number ' + vnum + ' in ' + cur.name + ' without option allow_alias = true, so the two states are the same byte on the wire.');
}
if (pwbReserved(cur, vnum)) {
add('reserved_number_reused', ln, 'Enum number ' + vnum + ' is reserved in ' + cur.name + '; a reader built before the deletion still reads that byte as the old state.');
}
if (cur.resNames[vn]) {
add('reserved_name_reused', ln, 'Enum value name "' + vn + '" is reserved in ' + cur.name + ' and cannot be brought back.');
}
if (cur.enumNums[vnum] === undefined) cur.enumNums[vnum] = { name: vn, line: ln };
cur.enumVals.push({ name: vn, num: vnum, line: ln });
continue;
}
}
var mb = nearestMessage();
var fm = line.match(PWB_FIELD_RE);
if (mb && fm) {
var mod = (fm[1] || '').trim(), type = fm[2].trim(), name = fm[3], num = parseInt(fm[4], 10), o = fm[5] || '';
if (num >= 19000 && num <= 19999) {
add('internal_range_19000', ln, 'Field number ' + num + ' on "' + name + '" sits in 19000-19999, the range protobuf keeps for itself; protoc refuses to compile it.');
} else if (num === 0) {
add('field_number_over_max', ln, 'Field number 0 on "' + name + '" is not a legal tag - field numbers start at 1.');
} else if (num > PWB_MAX_FIELD) {
add('field_number_over_max', ln, 'Field number ' + num + ' on "' + name + '" is above the maximum ' + PWB_MAX_FIELD + '.');
}
if (mb.fields[num] !== undefined) {
add('field_number_reuse', ln, 'Field number ' + num + ' already belongs to "' + mb.fields[num].name + '" on line ' + mb.fields[num].line + ' of ' + mb.name + '. One tag, two fields: a reader decodes "' + name + '" into ' + mb.fields[num].name + ' and never errors.');
} else {
mb.fields[num] = { name: name, line: ln };
mb.nums.push(num);
}
if (pwbReserved(mb, num)) {
add('reserved_number_reused', ln, 'Field number ' + num + ' is reserved in ' + mb.name + ', so it belonged to a deleted field. Every reader older than that deletion decodes tag ' + num + ' as the old type.');
}
if (mb.resNames[name]) {
add('reserved_name_reused', ln, 'Field name "' + name + '" is reserved in ' + mb.name + '. Reusing it silently re-points old JSON payloads and generated accessors at new data.');
}
if (mb.fnames[name] !== undefined) {
add('dup_field_name', ln, 'Field name "' + name + '" is already declared on line ' + mb.fnames[name] + ' of ' + mb.name + '; protoc rejects the file.');
} else {
mb.fnames[name] = ln;
}
var jm = o.match(/json_name\s*=\s*["']([^"']+)["']/);
var jn = jm ? jm[1] : pwbCamel(name);
if (mb.jnames[jn] !== undefined && mb.jnames[jn].name !== name) {
add('json_name_collision', ln, '"' + name + '" and "' + mb.jnames[jn].name + '" (line ' + mb.jnames[jn].line + ') both serialise to the JSON key "' + jn + '" in ' + mb.name + ', so JSON transcoding loses one of them.');
} else if (mb.jnames[jn] === undefined) {
mb.jnames[jn] = { name: name, line: ln };
}
if (mod === 'required') {
add('proto2_required', ln, '"' + name + '" is required. A required field can never be deleted or made optional later without breaking every reader that already validates it.');
}
if (syntax === 'proto3' && /\bdefault\s*=/.test(o)) {
add('proto3_default_value', ln, '"' + name + '" carries a proto2 default option, which proto3 does not support; the generated reader returns the type zero instead of the value written here.');
}
if (mod === 'repeated' && PWB_SCALAR[type] && /\bpacked\s*=\s*false\b/.test(o)) {
add('packed_explicit_override', ln, 'repeated ' + type + ' "' + name + '" turns packing off, which writes a different byte layout than the proto3 default for the same values.');
}
if (syntax === 'proto3' && !mod && PWB_SCALAR[type] && !inOneof() && PWB_PRESENCE_HINT.test(name)) {
add('presence_sensitive_no_optional', ln, '"' + name + '" is a bare proto3 ' + type + ', so the reader cannot tell "not sent" from ' + (type === 'string' ? '""' : (type === 'bool' ? 'false' : '0')) + '. Mark it optional before a client starts omitting it.');
}
continue;
}
continue;
}
for (var oi = 0; oi < opens; oi++) {
depth++;
blocks[depth] = pending ? pwbBlock(pending.kind, pending.name, pending.line) : pwbBlock('other', null, ln);
pending = null;
}
for (var ci = 0; ci < closes; ci++) {
if (blocks[depth]) { closed.push(blocks[depth]); delete blocks[depth]; }
if (depth > 0) depth--;
}
}
for (var bi = 0; bi < closed.length; bi++) {
var b = closed[bi];
if (b.kind === 'message') {
var runs = pwbGaps(b.nums);
for (var ri = 0; ri < runs.length; ri++) {
var lo = runs[ri][0], hi = runs[ri][1], nextNum = runs[ri][2];
if (hi - lo + 1 > PWB_GAP_MAX) continue;
var open = [];
for (var n = lo; n <= hi; n++) { if (!pwbReserved(b, n)) open.push(n); }
if (!open.length) continue;
add('unreserved_field_gap', b.fields[nextNum] ? b.fields[nextNum].line : b.line,
'Message ' + b.name + ' skips field ' + (open.length === 1 ? ('number ' + open[0]) : ('numbers ' + open[0] + '-' + open[open.length - 1])) + ' with no reserved statement. If a field was deleted there, the next edit will hand that tag to new data while old readers keep the old type.');
}
} else if (b.kind === 'enum') {
var enums = [];
for (var k in b.enumNums) { if (Object.prototype.hasOwnProperty.call(b.enumNums, k)) enums.push(parseInt(k, 10)); }
var eruns = pwbGaps(enums);
for (var ei = 0; ei < eruns.length; ei++) {
var elo = eruns[ei][0], ehi = eruns[ei][1], enext = eruns[ei][2];
if (ehi - elo + 1 > PWB_GAP_MAX) continue;
var eopen = [];
for (var m2 = elo; m2 <= ehi; m2++) { if (!pwbReserved(b, m2)) eopen.push(m2); }
if (!eopen.length) continue;
add('enum_unreserved_gap', b.enumNums[enext] ? b.enumNums[enext].line : b.line,
'Enum ' + b.name + ' skips ' + (eopen.length === 1 ? ('number ' + eopen[0]) : ('numbers ' + eopen[0] + '-' + eopen[eopen.length - 1])) + ' with no reserved statement, so a removed state can be reissued with a new meaning on the same byte.');
}
}
}
if (!syntax) add('missing_syntax', 1, 'No syntax declaration, so protoc compiles this file as proto2: field presence, defaults and enum rules all change under you.');
if (!hasPackage) add('missing_package', 1, 'No package declaration. Two files that both define a top-level name collide when they are linked into the same binary.');
findings.sort(function (a, b2) { return (a.line - b2.line) || (a.check < b2.check ? -1 : 1); });
var errors = 0, warns = 0;
for (var fi = 0; fi < findings.length; fi++) { if (findings[fi].sev === 'error') errors++; else warns++; }
return { findings: findings, errors: errors, warnings: warns,
ok: findings.length === 0, rule_count: RULES.length, today: opts.today || '' };
}
var PWB_API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof window !== 'undefined') { window.PWBENGINE = PWB_API; }
if (typeof module !== 'undefined' && module.exports) { module.exports = PWB_API; }
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var PWBREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = PWBREPORT;
else window.PWBREPORT = PWBREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.PWBENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.PWBREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.proto-wire-break-check" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'proto-wire-break-check',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/proto-wire-break-check" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'proto-wire-break-check',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/proto-wire-break-check" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'proto-wire-break-check',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/proto-wire-break-check" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'proto-wire-break-check',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/proto-wire-break-check" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'proto-wire-break-check',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.proto-wire-break-check
npx @readystack/proto-wire-break-check <file>
npx @readystack/proto-wire-break-check --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 17 rules Proto Wire Break Check checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"proto-wire-break-check",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Names the .proto edits that keep compiling and quietly change what old gRPC readers see</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A contract backend engineer runs $60+/hour on the public marketplaces, and reviewing one service's schema history by hand is an afternoon of that.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_qJoSHN0MM1nBgb9WtR2jW9kBTNvjLxyfKL1hP0NGwfK" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-17">Updated 2026-09-17</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-17","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku81241_result_card.jpg" alt="Proto Wire Break Check" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Eighteen findings in 27 lines. That is what the sample .proto in this repository returns, and every one of them is an edit a backend engineer who owns a gRPC service would wave through in a normal review.</p>
<p>Start with the one that costs the most, because no compiler will mention it. You delete a field from a message. The right move is to leave a reserved statement behind, so the number is spent forever. The common move is to delete the line, because the build is green either way. Six weeks later somebody adds a field and takes the next number that looks free - the one you just freed. Nothing throws. The service starts, the RPC returns, and every reader built before your deletion decodes the new data as the old type. A string arrives where an int64 used to be, and the parse either fails quietly or succeeds into the wrong field. You find out from a dashboard, days later, and the fix is a rollback.</p>
<p>A hole in the field numbers is legal proto. protoc has no opinion about it, and has never seen the mobile client you shipped last quarter - the only thing that makes the hole dangerous. That is the shape of the class: the edits that hurt are the legal ones, and the question is not "does this compile" but "does this change what an already-deployed reader sees".</p>
<p>Proto Wire Break Check applies 17 rules to the .proto in front of you and answers that second question, line by line.</p>
<p>Six are about the tag itself: a field number used twice in one message, a number the message already reserved, a reserved name taken back, an unreserved hole in the numbering, a number inside the protobuf-internal range 19000-19999, and a number outside 1 to 536870911. Two are about names: two fields that serialise to the same JSON key - invoice_id and invoiceId both become invoiceId - and two fields that share a name outright. Three are about enums, where the damage is quietest: a proto3 enum whose first value is not 0, so an unset field decodes as a real state instead of "unspecified"; two values on one number without allow_alias; and a removed state whose number was never reserved, free to be reissued on the same byte. The last six are presence and syntax: no syntax line, so the file silently compiles as proto2; no package; a required field, which can never be removed later; a proto2 default on a proto3 field; a bare proto3 scalar where absent and zero differ, such as amount_cents; and packed = false on a repeated scalar, which writes different bytes for the same values.</p>
<p>The measurement is in the repository, so you can repeat it. _fixtures/dirty.proto is 27 lines and returns 18 findings - 10 errors and 8 warnings - firing 13 of the 17 rules. _fixtures/clean.proto is the same schema written safely and returns 0. One of those 18 reads: field number 2 already belongs to "amount_cents" on line 10 of Invoice.</p>
<p>The same engine runs in the editor and in the browser, byte for byte. The free version checks the file you have open: all 17 rules, every finding, every line number, no key and no account. For one file that is the entire job.</p>
<p>What it does not do is answer the question across a repository at review time. The full version sweeps every .proto in the workspace in one pass and writes PROTO-WIRE-AUDIT.md plus audit.json - one artefact you can commit, attach to a pull request, or fail a build on. It is $29 once, one licence key per person or team seat. The yardstick: a contract backend engineer runs $60+/hour on the public marketplaces, and answering "which field numbers in this service are already spent" by hand is an afternoon of that.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku81241_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku81241_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Proto Wire Break Check actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a .proto file and applies 17 rules that catch edits which still compile but change what an already-deployed reader sees: a field number used twice, a reserved number taken back, two fields that share a JSON key, a proto3 enum that does not start at 0. Each finding names the line and the edit that keeps the wire intact.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Backend and platform engineers who own a gRPC or Connect service and review .proto changes from teammates or AI agents. If a schema edit of yours ships to a mobile client, a partner integration, or a Kafka topic you cannot redeploy in the same hour, this is the class of mistake that costs you a rollback.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is protoc or a chatbot not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Roughly half of these edits are legal proto, so protoc never mentions them - an unreserved hole in the numbering, a bare scalar where absent and zero differ, a removed enum state. The rest fail a build you have to wait for. And a language model tells you the file compiles, which is true and is not the question: it cannot know which tags your deployed readers still write.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free and without a key: the whole check on the file you have open, all 17 rules, every finding with line numbers. The 27-line sample file returns 18 findings, 10 of them errors. The licence key adds the workspace-wide sweep and the written PROTO-WIRE-AUDIT.md plus audit.json you can commit or gate a build on.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost me otherwise?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A contract backend engineer runs $60+/hour on the public marketplaces, and walking one service's schema history to answer which field numbers are already spent is an afternoon of that. The full version is $29 once, one licence key per person or team seat,.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Protobuf Breaking Change Lint - wire compatibility (.proto) checks the file you open against 17 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Proto Wire Break Check actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a .proto file and applies 17 rules that catch edits which still compile but change what an already-deployed reader sees: a field number used twice, a reserved number taken back, two fields that share a JSON key, a proto3 enum that does not start at 0. Each finding names the line and the edit that keeps the wire intact."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Backend and platform engineers who own a gRPC or Connect service and review .proto changes from teammates or AI agents. If a schema edit of yours ships to a mobile client, a partner integration, or a Kafka topic you cannot redeploy in the same hour, this is the class of mistake that costs you a rollback."}}, {"@type": "Question", "name": "Why is protoc or a chatbot not enough?", "acceptedAnswer": {"@type": "Answer", "text": "Roughly half of these edits are legal proto, so protoc never mentions them - an unreserved hole in the numbering, a bare scalar where absent and zero differ, a removed enum state. The rest fail a build you have to wait for. And a language model tells you the file compiles, which is true and is not the question: it cannot know which tags your deployed readers still write."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Free and without a key: the whole check on the file you have open, all 17 rules, every finding with line numbers. The 27-line sample file returns 18 findings, 10 of them errors. The licence key adds the workspace-wide sweep and the written PROTO-WIRE-AUDIT.md plus audit.json you can commit or gate a build on."}}, {"@type": "Question", "name": "What would this cost me otherwise?", "acceptedAnswer": {"@type": "Answer", "text": "A contract backend engineer runs $60+/hour on the public marketplaces, and walking one service's schema history to answer which field numbers are already spent is an afternoon of that. The full version is $29 once, one licence key per person or team seat,."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Protobuf Breaking Change Lint - wire compatibility (.proto) checks the file you open against 17 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:81241,slug:"proto-wire-break-check",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/dsa-terms-lint"><span class="yj-rel-t">DSA Terms Lint - EU Digital Services Act</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/gitlab-ci-break-lint"><span class="yj-rel-t">GitLab CI Break Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/mcp-config-guard"><span class="yj-rel-t">MCP Config Guard - agent config lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/about-page-template"><span class="yj-rel-t">About Page Template — 6 Finished About Pages for Small Service Businesses (Sel</span><span class="yj-rel-p">$49</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bookkeeper-website-template"><span class="yj-rel-t">Bookkeeper Website Template — 25 Ready-Written HTML Pages for a Bookkeeping Pr</span><span class="yj-rel-p">$49</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/contact-page-template"><span class="yj-rel-t">Contact Page Template — 20 Ready-to-Upload Contact Pages for Small Service Bus</span><span class="yj-rel-p">$49</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>
|