Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1a537a2
feat(ir): carry argument paths, element values, cardinality and union…
claude Aug 14, 2026
eeb3624
fix(scan): keep an argument path off any frame it cannot name
claude Aug 14, 2026
4faad42
fix(scan): tell an empty argument prefix apart from an unresolvable one
claude Aug 14, 2026
2a623a1
fix(scan): hold the argument-object boundary at the request root
claude Aug 14, 2026
827564e
feat(scan): address values inside map callbacks and on a stanza root
claude Aug 14, 2026
a5b6225
fix(ir): record an optional literal's value instead of addressing its…
claude Aug 14, 2026
df35ffe
fix(scan): rebase a mixin contribution like every other handed frame
claude Aug 15, 2026
42a9045
test(scan): make the coverage guard look at variant-group attributes
claude Aug 15, 2026
c1ab69a
fix(scan): close three latent misreads at the builder boundary
claude Aug 15, 2026
c0cf801
feat(scan): rebase a mixin contribution onto the frame that named it
claude Aug 15, 2026
c33e3cf
fix(ir): keep a pinned payload's address, and refuse two addresses fo…
claude Aug 15, 2026
20206f2
Merge origin/main into the request-builder batch
claude Aug 15, 2026
2cc395d
fix(scan): shadow per scope, and withhold a sibling builder's fragment
claude Aug 15, 2026
b9007ce
feat(ir): address the request's addressee, and stop a wrapper claimin…
claude Aug 15, 2026
60e5e55
fix(scan): key a mixin reach by its frame, and dedupe stanzas by addr…
claude Aug 15, 2026
43a7e86
fix(scan): bound the mixin walk by module, and let a local `to` own i…
claude Aug 15, 2026
03d67e2
fix(ir): withdraw an addressee's key when the chain disagrees about t…
claude Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,17 @@ Note the contract version: this raised `schemaVersion` to **2.0.0**, and it is a
- A response variant's `errorCodes` / `errorTexts` / `errorCodeMin` / `errorCodeMax` / `errorClass` are **gone**, replaced by `errorArms` (+ `errorEnvelope`). The flat lists were removed rather than kept alongside because they were unsound: two independent lists cannot say which code goes with which text, and 117 variants admitted combinations the parser rejects.
- `ContentType` gained `integer`, the same closed-enum widening as the first item: a `<registration>` whose body is a number used to be reported as `string`. Live in the response children that read a big-endian integer content (`contentUint`).

Anything the extractor sees but cannot resolve structurally is counted under `manifest.diagnostics.iq.dropsByReason` rather than omitted, so "no constraint here" and "a constraint we failed to extract" never look alike. `manifest.diagnostics.iq.constraints` and `diagnostics.notif.actions` are floor-guarded: a WA refactor that hides one of these constructs fails the update instead of silently emptying a field.
### Enough to *call* WhatsApp's own builder, not only to encode the stanza yourself

Everything above describes the wire. That serves a client that encodes the stanza itself — it needs to know a group create carries a `<participant jid=…>` of type `user_jid`. It does not serve a client that **runs WhatsApp's own modules**, which needs the other half: that the value goes in `args.participantArgs[].participantJid`. Neither half implies the other — WA picks the argument key independently of the attribute it lands in (`subjectElementValue` becomes the *text* of `<subject>`) — and almost every request is composed out of mixins, so the argument key is usually defined in a different module from the tag it fills. So the request side also carries the builder:

- **Argument paths** (`argPath` on a request node, an attribute, or an element content) — the absolute path from the builder's argument object, as segments. `list` marks the one segment that is indexed, and only that one: `REPEATED_CHILD(template, list, min, max)` calls the template once per element, while `OPTIONAL_CHILD`/`HAS_OPTIONAL_CHILD` hand the object over whole. The same suffix in the wrong place writes the value where the vendor builder never reads it, and the stanza goes out without it. Recovered structurally — from the function's single argument parameter and the `var x = <param>.<key>` destructure — never from a name: `…Args`, `has…` and `any…` are WA conventions that make the IR readable, not evidence. A path that isn't structurally recoverable is absent and counted under `manifest.diagnostics.iq.builder`, never guessed. The legacy `WAWeb*Job` builders take positional parameters rather than one options object, so they get no path at all and are counted as such.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document every nested list segment

This says exactly one segment in an argument path is indexed, but nested repeated combinators legitimately produce multiple indexed segments. The committed WASmaxOutPreKeysFetchMissingPreKeysRequest already emits userArgs[] → deviceArgs[] → deviceId; a consumer following this guidance may reject that path or index only one level and place the device ID on the wrong object. Describe list as marking each segment whose corresponding combinator iterates, rather than asserting there can be only one per path.

Useful? React with 👍 / 👎.

- **Cardinality of a request child** (`presence`, plus `repeatMin`/`repeatMax`) — the four states `WASmaxChildren` distinguishes, of which the wire shows one. `<locked/>` on a group create is a *presence marker*: its template takes no arguments, its whole meaning is being there, and a consumer can model it as a `bool` — which it must not do for an optional child and cannot do for an empty required one, all three of which are the same empty element on the wire. The repeat bounds are the ones a server enforces (`add/participant` 1..1024, `query/group` 1..10000, `media_list` 0..10); a `repeatMin` with no `repeatMax` is WA's explicit `1/0`, i.e. unbounded above, which stays distinguishable from a child that states no bound at all and has neither.
- **Element values that survive the mixin boundary** (`content`) — `smax("subject", null, subjectElementValue)` is the entire payload of a group rename. WA's builders bind that payload to a local before writing it, and a bare local used to be ignored outright in case it was a node variable. It is now told apart structurally: a local that resolves to an argument path is content, one that resolves to a `smax(…)` call is a child.

Note the contract version: this raised `schemaVersion` to **2.1.0**, and the minor is checked rather than assumed. Every new property is optional and skipped at its default, no existing field's value space widened, and each committed `*/index.json` validates clean against its own **2.0.0** schema (0 errors across all 11 domains) — so a 2.0 consumer reads the new documents unchanged and needs no migration. One field changed *value* rather than shape: `content` is now populated on 38 request nodes that previously carried none, an optional field filled in where the builder does supply a payload. A consumer that treated "no `content`" as "this request has no body" was already wrong about those requests; it is now told so.

Anything the extractor sees but cannot resolve structurally is counted under `manifest.diagnostics.iq.dropsByReason` rather than omitted, so "no constraint here" and "a constraint we failed to extract" never look alike. `manifest.diagnostics.iq.constraints`, `diagnostics.iq.builder` and `diagnostics.notif.actions` are floor-guarded: a WA refactor that hides one of these constructs fails the update instead of silently emptying a field.

## Quick start

Expand Down
24 changes: 24 additions & 0 deletions crates/wa-codegen/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2465,6 +2465,7 @@ mod tests {
value: value.map(|v| v.into()),
required: false,
enum_ref: None,
arg_path: None,
}
}
fn leaf(tag: &str) -> WapChildNode {
Expand All @@ -2475,6 +2476,7 @@ mod tests {
content: None,
repeats: false,
variant_groups: vec![],
..Default::default()
}
}

Expand Down Expand Up @@ -2563,6 +2565,7 @@ mod tests {
content: None,
repeats: true,
variant_groups: vec![],
..Default::default()
};
let node = WapChildNode {
tag: "config".into(),
Expand All @@ -2577,6 +2580,7 @@ mod tests {
children: vec![item],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -2887,6 +2891,7 @@ mod tests {
content: None,
repeats: false,
variant_groups: groups,
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3005,6 +3010,7 @@ mod tests {
content: None,
repeats: true,
variant_groups: vec![],
..Default::default()
};
let node = WapChildNode {
tag: "config".into(),
Expand All @@ -3019,6 +3025,7 @@ mod tests {
children: vec![kid("type"), kid("2fa")],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3056,6 +3063,7 @@ mod tests {
children: vec![kid("type")],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3089,6 +3097,7 @@ mod tests {
children: vec![kid("my-tag")],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3121,6 +3130,7 @@ mod tests {
content: None,
repeats: true,
variant_groups: vec![],
..Default::default()
};
let node = WapChildNode {
tag: "config".into(),
Expand All @@ -3135,6 +3145,7 @@ mod tests {
children: vec![kid("foo-bar"), kid("foo_bar")],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3173,6 +3184,7 @@ mod tests {
children: vec![kid("item")],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let mut ctx = VariantCtx {
Expand Down Expand Up @@ -3205,6 +3217,7 @@ mod tests {
content: None,
repeats: true,
variant_groups: vec![],
..Default::default()
};
let node = WapChildNode {
tag: "config".into(),
Expand All @@ -3219,6 +3232,7 @@ mod tests {
children: vec![item],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3265,6 +3279,7 @@ mod tests {
children: vec![nested],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3310,6 +3325,7 @@ mod tests {
children: vec![nested],
}],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3486,6 +3502,7 @@ mod tests {
],
},
],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3567,6 +3584,7 @@ mod tests {
optional: false,
variants: vec![mk("appid"), mk("voip"), mk("endpoint")],
}],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3788,6 +3806,7 @@ mod tests {
content: None,
repeats: false,
variant_groups: vec![],
..Default::default()
};
let (lines, var, _) = build1(&child);
let code = lines.join("\n");
Expand Down Expand Up @@ -3820,6 +3839,7 @@ mod tests {
content: None,
repeats: false,
variant_groups: vec![],
..Default::default()
};
let (lines, _, _) = build1(&parent);
let code = lines.join("\n");
Expand Down Expand Up @@ -3850,6 +3870,7 @@ mod tests {
}),
repeats: false,
variant_groups: vec![],
..Default::default()
};
let (lines, _, _) = build1(&node);
let code = lines.join("\n");
Expand Down Expand Up @@ -3878,6 +3899,7 @@ mod tests {
}),
repeats: false,
variant_groups: vec![],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3953,6 +3975,7 @@ mod tests {
}),
repeats: false,
variant_groups: vec![],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down Expand Up @@ -3984,6 +4007,7 @@ mod tests {
}),
repeats: false,
variant_groups: vec![],
..Default::default()
};
let (mut enums, mut fields) = (Vec::new(), Vec::new());
let reserved = std::collections::HashSet::new();
Expand Down
2 changes: 2 additions & 0 deletions crates/wa-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ mod tests {
value: None,
required: true,
enum_ref: None,
arg_path: None,
}
}

Expand Down Expand Up @@ -736,6 +737,7 @@ mod tests {
content: None,
repeats: false,
variant_groups: vec![],
..Default::default()
}],
},
response: ParsedResponse {
Expand Down
2 changes: 2 additions & 0 deletions crates/wa-codegen/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1115,11 +1115,13 @@ mod tests {
value: None,
required: true,
enum_ref: None,
arg_path: None,
}],
children: vec![],
content: None,
repeats: false,
variant_groups: vec![],
..Default::default()
};
let children = vec![
node("participant", WapAttrKind::UserJid),
Expand Down
2 changes: 2 additions & 0 deletions crates/wa-codegen/src/stanza_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ mod tests {
module: module.into(),
variants: vars,
}),
arg_path: None,
}
}

Expand All @@ -292,6 +293,7 @@ mod tests {
value: None,
required: true,
enum_ref: None,
arg_path: None,
}
}

Expand Down
Loading
Loading