P1: coverage gap-fills — proto validation guard, wire enums, message/receipt mixin catalog, conditional children - #24
Conversation
WAProto.proto was the only generated artifact with zero validation in CI: the JSON domains are schema-checked by scripts/validate-schemas.py, but the .proto was never parsed. Since update.yml auto-regenerates and auto-commits on new WhatsApp versions with no human review, a future bundle change that breaks extraction (dangling type ref, duplicate name, mis-scoped enum values — the failure class the enum-relocation pass in extract.rs risks) would be committed silently. Add protox (pure-Rust protoc) as a wa-proto dev-dependency and a test that compiles the committed WAProto.proto, resolving every cross-reference. It's C-free (no ring/aws-lc/openssl-sys) and dev-only, so it touches neither the wasm-safe wa-ir contract nor the C-free CI guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013111jePsSca7epxMugRn2A
Some wire enums are plain object literals exported by name
(`var e={INACTIVE:-6,…}; i.ACK=e`) with no `$InternalEnum` dependency, so
the catalog (scoped to `$InternalEnum` for safety) never saw them:
`WAWebAck.ACK`/`ACK_STRING`/`EDIT_ATTR`, `WAAckLevel.ACK`, and the
`WASmaxIn…Enums` bags (receipt, message-deliver/publish, account, biz
payment/marketing/settings, …).
A plain object is ambiguous — every `{a:1,b:2}` config/UI/i18n map looks
like one — so this pass is deliberately narrow: gated on WA's own protocol
enum-bag naming convention (`WASmax<In|Out>…Enums`, plus the two ack-level
modules), and within those only exports whose members are all CONSTANT_CASE
are kept. An untargeted plain-object scan pulls in ~190 infra constants
(loggers, locales, JPEG markers, QR levels, `.pb` proto duplicates); the
convention gate keeps exactly the 31 genuine protocol enums (257 -> 288).
The Cargo.lock update is the protox dev-dependency added in the prior commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013111jePsSca7epxMugRn2A
The stanza scanner dropped every `merge…Mixin` module whole, so the smax
message/receipt content-type surface — which WA composes at runtime from
per-type mixin fragments (`smax("message",{type:"reaction"})`, media, poll,
quiz, revoke; receipt view/newsletter/status-class; status content types) —
was invisible. Only the newsletter/spam whole-stanza roots survived.
Keep these fragments instead of dropping them, marked with a new
`StanzaDef.fragment` flag (default false, omitted when serialized so the 84
standalone stanzas stay byte-identical). This adds 90 catalogued fragments
(52 message, 22 status, 7 ack, 3 receipt, 3 privacy, 2 chatstate, 1 call),
including the receipt view-publish mixins (`WASmaxOutReceiptViewTypeMixin`,
type=view) that a newsletter-view receipt request composes. A consumer
building a sendable stanza filters `fragment: true` out.
Verified: standalone stanzas byte-identical to before; output deterministic;
0 structureless fragments; the JSON schema gains the `fragment` field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013111jePsSca7epxMugRn2A
`resolve_child_node` traced variable refs, arrays, and `.map()` templates but
not a `cond ? A : B` child, so any child attached conditionally was dropped.
WA's aggregate receipt/ack builder does exactly this
(`a.length > 0 ? wap("list", …) : null`), and several message/iq builders
gate optional children the same way.
Resolve the branch that yields children (consequent first, then the alternate),
so these children are recovered. Additive across the board — no child is
removed: iq gains 2 conditional children (`<gdpr>`, `<picture type=image>`),
and stanza gains children on 8 builders (the aggregate ack's `<list><item>`,
receipt `<meta>`/`<bot>`, message `<meta>`/`<key>`/`<sender_content_binding>`/
`<interactive>`/`<native_flow>`). Shared with the iq path; verified the iq
delta is exactly those 2 additions and nothing else.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013111jePsSca7epxMugRn2A
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughAdds a ChangesStanza Fragment Tracking
Conditional Child Node Resolution
Protocol Enum Extraction
Proto Compile Validation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/wa-scan/src/stanza.rs (1)
69-72: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winStale comment: fragment modules are no longer "dropped whole in
scan_module".This comment still asserts the old behavior.
scan_modulenow keeps fragment-module stanzas and tags them viafragment; only the generic structureless filter below removes them (if they truly have no attrs/children/subtype). Update the comment to avoid confusing future readers about the fragment classification.✏️ Suggested comment fix
- // Drop structureless stanzas (no attrs, no children, no subtype) — a bare `<ack/>` - // carries nothing a consumer can model. (Fragment `merge…Mixin` modules are already - // dropped whole in `scan_module`.) + // Drop structureless stanzas (no attrs, no children, no subtype) — a bare `<ack/>` + // carries nothing a consumer can model. Fragment `merge…Mixin` modules are now kept + // and marked via `StanzaDef::fragment`, so they're only dropped here if genuinely + // structureless.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/wa-scan/src/stanza.rs` around lines 69 - 72, The comment in `stanza.rs` is stale because `scan_module` no longer drops fragment modules wholesale; update the note above the `out.retain` filter to describe the current behavior in `scan_module` and `merge…Mixin` handling, making clear that fragment-module stanzas are kept and tagged via `fragment`, while this filter only removes truly structureless stanzas with no attrs, children, or subtype. Keep the comment aligned with the logic around `out.retain` and `scan_module` so future readers are not misled about fragment classification.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/wa-enums/src/lib.rs`:
- Around line 188-198: Add a regression test for extract_plain_object_enums that
covers a plain-object export with mixed literal kinds, such as one int and one
string variant, and verify it is excluded from the extracted enums. Use the
existing plain-object enum parsing path in crates/wa-enums/src/lib.rs and mirror
the mixed-skip expectation already covered by
default_export_named_by_module_and_mixed_skipped so the shared parsing logic is
protected.
- Around line 175-187: The pending-resolution loop in the resolver is duplicated
between this code path and resolve_named_enum, so factor the shared logic into a
helper on NamedResolver (for example, resolve_pending) and call it from both
places. Move the iteration over r.pending and the r.locals/r.exports update into
that helper, keeping the behavior identical while removing the duplicated block.
In `@crates/wa-scan/src/request.rs`:
- Around line 219-249: The conditional-child branch in resolve_child_node adds
two more repeated recursive calls with the same long argument list. Refactor the
local recursion pattern by introducing a small helper closure or local wrapper
that captures the shared context arguments (node_source, scope, module_source,
aliases, contributions, helpers, depth), then use it for the
Expression::ConditionalExpression path and the existing similar cases in
resolve_child_node to reduce boilerplate and make the function easier to read.
- Around line 219-249: The conditional handling in resolve_child_node currently
returns the consequent branch immediately when it resolves non-empty, which can
drop a valid alternate branch for ternaries like cond ? wap("a", …) : wap("b",
…). Update the Expression::ConditionalExpression path to inspect both
cond.consequent and cond.alternate in the same logic, and merge children when
both sides resolve to non-empty instead of early-returning. Keep the existing
fallback behavior for cond ? node : null / cond ? null : node, but ensure
genuinely divergent branches in request.rs are both preserved.
---
Outside diff comments:
In `@crates/wa-scan/src/stanza.rs`:
- Around line 69-72: The comment in `stanza.rs` is stale because `scan_module`
no longer drops fragment modules wholesale; update the note above the
`out.retain` filter to describe the current behavior in `scan_module` and
`merge…Mixin` handling, making clear that fragment-module stanzas are kept and
tagged via `fragment`, while this filter only removes truly structureless
stanzas with no attrs, children, or subtype. Keep the comment aligned with the
logic around `out.retain` and `scan_module` so future readers are not misled
about fragment classification.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: df69b57a-64f8-4ca9-84ad-ab77d8c773a7
⛔ Files ignored due to path filters (6)
Cargo.lockis excluded by!**/*.lockgenerated/enums/index.jsonis excluded by!**/generated/**generated/iq/index.jsonis excluded by!**/generated/**generated/manifest.jsonis excluded by!**/generated/**generated/schema/stanza.schema.jsonis excluded by!**/generated/**generated/stanza/index.jsonis excluded by!**/generated/**
📒 Files selected for processing (7)
crates/wa-codegen/src/stanza_export.rscrates/wa-enums/src/lib.rscrates/wa-ir/src/iq.rscrates/wa-proto/Cargo.tomlcrates/wa-proto/tests/proto_compiles.rscrates/wa-scan/src/request.rscrates/wa-scan/src/stanza.rs
|
| Filename | Overview |
|---|---|
| crates/wa-enums/src/lib.rs | Adds plain-object enum extraction gated on WA's protocol-module naming convention; resolve_pending correctly extracted and shared; CONSTANT_CASE filter and ≥2 members guard prevent noise; tests cover all boundary cases. |
| crates/wa-scan/src/stanza.rs | Switches is_fragment_module from all→any and marks all stanzas in a module uniformly; PR validates standalone set is byte-identical to before, but the any rule could silently misclassify a future WA module that exports both a mixin and a standalone builder. |
| crates/wa-scan/src/request.rs | Adds ternary child resolution that unions both branches; passes existing depth (not depth+1) consistently with other non-function-call cases; recursion is bounded by existing MAX_FN_DEPTH guards in the function-trace helpers; well-covered by new tests. |
| crates/wa-proto/tests/proto_compiles.rs | Proto compile guard correctly panics under CI when the file is absent; uses protox::compile with include-root for full cross-reference resolution; sanity-checks a non-empty descriptor set. |
| crates/wa-ir/src/iq.rs | Adds fragment: bool field to StanzaDef with serde(default, skip_serializing_if = "Not::not") so existing 84 standalone stanzas remain byte-identical in serialized form. |
| crates/wa-codegen/src/stanza_export.rs | Test fixtures updated to include fragment: false on existing StanzaDef structs; no logic changes. |
| crates/wa-proto/Cargo.toml | Adds protox = "0.9" as a dev-dependency only; C-free and wasm-safe surface unaffected. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Bundle Source] --> B[extract_module_definitions]
B --> C{Module type?}
C -- has $InternalEnum dep --> D[extract_from_module\nInternalEnum catalog path]
C -- is_protocol_enum_module\nWASmaxIn/OutEnums\nWAWebAck / WAAckLevel --> E[extract_plain_object_enums\nCONSTANT_CASE filter\n≥2 members guard]
C -- other --> F[skip]
D --> G[EnumsIr\n257→288 enumDefs]
E --> G
B --> H[scan_stanzas_from_modules]
H --> I{is_fragment_module?\nany export starts with merge + Mixin}
I -- yes fragment=true --> J[StanzaDef fragment=true\n90 new mixin fragments]
I -- no fragment=false --> K[StanzaDef fragment=false\n84 existing standalone stanzas]
J --> L[retain: not structureless]
K --> L
L --> M[StanzaIr\n84→174 stanzaDefs]
H --> N[resolve_child_node]
N --> O{Expression type?}
O -- Parenthesized --> N
O -- ConditionalExpression\ncond ? A : B --> P[resolve consequent\n+ resolve alternate\nunion deduplicate]
O -- wap/smax call --> Q[WapChildNode]
O -- Array --> R[flatten elements]
O -- Identifier ref --> S[trace var initializer]
O -- .map call --> T[resolve map template]
P --> Q
R --> Q
S --> Q
T --> Q
U[WAProto.proto] --> V[protox::compile\nparse + resolve all cross-refs]
V -- failure --> W[panic: broken schema]
V -- success --> X[assert non-empty descriptor set]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[Bundle Source] --> B[extract_module_definitions]
B --> C{Module type?}
C -- has $InternalEnum dep --> D[extract_from_module\nInternalEnum catalog path]
C -- is_protocol_enum_module\nWASmaxIn/OutEnums\nWAWebAck / WAAckLevel --> E[extract_plain_object_enums\nCONSTANT_CASE filter\n≥2 members guard]
C -- other --> F[skip]
D --> G[EnumsIr\n257→288 enumDefs]
E --> G
B --> H[scan_stanzas_from_modules]
H --> I{is_fragment_module?\nany export starts with merge + Mixin}
I -- yes fragment=true --> J[StanzaDef fragment=true\n90 new mixin fragments]
I -- no fragment=false --> K[StanzaDef fragment=false\n84 existing standalone stanzas]
J --> L[retain: not structureless]
K --> L
L --> M[StanzaIr\n84→174 stanzaDefs]
H --> N[resolve_child_node]
N --> O{Expression type?}
O -- Parenthesized --> N
O -- ConditionalExpression\ncond ? A : B --> P[resolve consequent\n+ resolve alternate\nunion deduplicate]
O -- wap/smax call --> Q[WapChildNode]
O -- Array --> R[flatten elements]
O -- Identifier ref --> S[trace var initializer]
O -- .map call --> T[resolve map template]
P --> Q
R --> Q
S --> Q
T --> Q
U[WAProto.proto] --> V[protox::compile\nparse + resolve all cross-refs]
V -- failure --> W[panic: broken schema]
V -- success --> X[assert non-empty descriptor set]
Reviews (2): Last reviewed commit: "review: address cubic/greptile/coderabbi..." | Re-trigger Greptile
There was a problem hiding this comment.
5 issues found across 13 files
Confidence score: 3/5
crates/wa-scan/src/request.rsstill returns early in the ternary-child path, so when both branches build children the alternate can be skipped and valid schema entries are lost; merging as-is risks an incomplete stanza-child catalog for downstream consumers — update the branch handling to collect children from both consequent and alternate before returning.crates/wa-scan/src/stanza.rscan now classify mixin modules with helper exports as sendable stanzas (fragment: false), which may let partial/non-sendable stanzas pass fragment-based filters; that creates concrete misclassification risk in consumer logic — align the sendable-stanza predicate with the stricter scanner criteria before merge.- In
crates/wa-enums/src/lib.rs, the new plain-object enum path has weaker module-name gating and missing mixed-value rejection coverage, so false positives are more likely and regressions may slip through; the duplicated pending-resolution loop also increases drift risk — tighten theWASmaxIn...Enums/WASmaxOut...Enumspredicate and add a mixed-value test (then optionally factor the shared loop in a follow-up).
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
Correctness (all three bots): - request.rs: a two-branch ternary child `cond ? A : B` now unions BOTH branches instead of returning the consequent early, so a genuinely divergent child catalogs every shape it can take (still yields just the node for the common `cond ? node : null`). Recovers a 2nd `<gdpr>` variant on getGdprIq and the alternate `<key rkid>` on two message builders. - stanza.rs: `is_fragment_module` now uses `any(merge…Mixin)`, not `all` (matching the IQ scanner after the P1-A fix). A mixin that also exports a `make…` helper was misclassified as a standalone sendable stanza; 7 such `WASmaxOut*Mixin` modules are now correctly marked `fragment` (stanza standalone 84→77, fragment 90→97, total unchanged at 174). Robustness / hygiene: - proto_compiles: a missing committed .proto now fails loudly under CI (env `CI` set) instead of a stderr-only skip that `cargo test` hides — the guard can no longer be silently bypassed. Still tolerant locally. - enums: tighten the plain-object gate to the documented `WASmaxIn…Enums` / `WASmaxOut…Enums` convention (was `WASmax…Enums`); no output change (all 31 are `WASmaxIn*`). Factor the duplicated pending-resolution loop into a shared `resolve_pending` helper. Tests: mixed int/string plain-object enum rejection; make+merge fragment classification; both-branch ternary child capture. Fix a stale comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013111jePsSca7epxMugRn2A
There was a problem hiding this comment.
0 issues found across 7 files (changes from recent commits).
Requires human review: Major refactor of core extraction logic with a proto guard and new enum path; artifacts and schema change, and a bug could silently corrupt protocol metadata, so manual review is required.
Re-trigger cubic
Summary
Follow-up to #22 (P0) and #23 (P1 pipeline-trust). This closes out the remaining P1 items — but the re-evaluation changed what they are. The original framing ("build new
message/receiptdomains, fix proto correctness") didn't survive empirical validation:message/receiptdomains would duplicate whatstanza/incoming/srvreqalready extract — the genuinely-uncaptured slice is the mixin fragments the pipeline deliberately drops..protocompiles under protoc 35 and protox). The real hole was the absence of a validation guard, not extraction correctness.So this PR is four targeted gap-fills, each validated against the real bundle and shipped as its own commit.
What's in it
1. Proto validation guard (
test(proto))WAProto.protowas the only generated artifact with no CI validation (JSON domains are schema-checked; the.protowas never even parsed). Sinceupdate.ymlauto-regenerates and auto-commits on new WA versions with no review, a future extraction break (dangling type ref, dup name, mis-scoped enum) would land silently. Addsprotox(pure-Rust protoc) as awa-protodev-dependency and a test that compiles the committedWAProto.proto, resolving every cross-reference. C-free (no ring/aws-lc/openssl-sys) and dev-only, so it touches neither the wasm-safewa-ircontract nor the C-free CI guard. A missing.protohard-fails under CI (so the guard can't be silently skipped) while tolerating local sparse checkouts.2. Plain-object wire enums (
feat(enums)) — enumDefs 257 → 288Some wire enums are plain object literals with no
$InternalEnumdep, so the catalog never saw them:WAWebAck.ACK/ACK_STRING/EDIT_ATTR,WAAckLevel.ACK, and theWASmaxIn…Enumsbags (receipt, message-deliver/publish, account, biz payment/marketing/settings). A plain object is ambiguous — an untargeted scan pulls in ~190 infra constants (loggers, locales, JPEG markers,.pbproto-duplicates) — so the pass is gated on WA's own protocol enum-bag naming convention (WASmaxIn…Enums/WASmaxOut…Enums+ the two ack-level modules) and keeps only CONSTANT_CASE members. Result: exactly +31 genuine protocol enums, 0 noise.3. Message/receipt content-type mixin catalog (
feat(stanza)) — stanzaDefs 84 → 174The stanza scanner dropped every
merge…Mixinmodule whole, so the smax message/receipt content-type surface (which WA composes at runtime from per-type mixin fragments —{type:"reaction"}, media, poll, quiz, revoke; receipt view/newsletter/status-class) was invisible. These are now kept, marked with a newStanzaDef.fragmentflag (default false, omitted when serialized). Adds 90 catalogued fragments, including the receipt view-publish mixins a newsletter-view receipt request composes. Consumers building a sendable stanza filterfragment: trueout.The fragment classifier uses
any(merge…Mixin), notall(matching the iq scanner after #23's P1-A fix): a mixin that also exports amake…helper was otherwise misclassified as a standalone sendable stanza. Net split after this fix: 77 standalone + 97 fragment = 174 (7WASmaxOut*Mixinmodules correctly moved standalone→fragment; the standalone content itself is otherwise unchanged).4. Conditional (ternary) stanza children (
feat(scan))resolve_child_nodetraced variable refs, arrays, and.map()templates but not acond ? A : Bchild, so any conditionally-attached child was dropped — e.g. the aggregate receipt/ack builder'sa.length > 0 ? wap("list", …) : null. Now resolves both branches and unions their children (dedup'd): the commoncond ? node : nullyields just the node, while a genuinely divergentcond ? A : Bcatalogs both shapes rather than silently dropping one. Purely additive: iq gains conditional children (<gdpr>variants,<picture type=image>) and 8 stanza builders gain children (the aggregate ack's<list><item>, receipt<meta>/<bot>, message<meta>/<key>/<sender_content_binding>/<interactive>/<native_flow>). Shared with the iq path; verified the iq delta is exactly those additions and nothing else.Validation
Every output-changing commit was regenerated against the real bundle (version pinned) and diffed:
--checkbyte-identical.Full CI-equivalent gate is green locally:
fmt,clippy --workspace --all-targets --all-features -D warnings,cargo test --workspace(32 suites),wa-ir --features schema, wa-ir/wa-fetch wasm32,whatspec --no-default-features, C-free,validate-schemas.py(11 domains), and the byte-identical determinism check.🤖 Generated with Claude Code
https://claude.ai/code/session_013111jePsSca7epxMugRn2A