Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ Every domain ships a JSON Schema under `generated/schema/`, and a top-level `gen
Field shapes tell you how to *read* a stanza. They are not enough to *produce* one, nor to explain why a real client rejected the one you produced. So the IR also carries the rules WA Web's own parsers enforce — symmetric by construction, so they serve a parser and an emitter equally:

- **Echo rules** (`assertions[].kind == "reference"`, and `referencePath` on a field) — an answer's `from` must equal the **request's** `to`, its `id` the request's `id`. `referencePath` is the argument list of WA's `attrStringFromReference`, so `["to"]` means "the request's `to`" and `["account","action"]` means "the `action` attribute of the request's `<account>` child" — no name-matching required. An emitter that hardcodes `from="s.whatsapp.net"` makes every answer to a `g.us` request unparseable.
- **Pinned values** (`literalValue` on a field) — `type="admin"` on a successful promote, `matched="true"`/`"false"` on a blocklist update, `code=429` on a rate-limit error. `required` separates the two forms: a required pin is a hard discriminator (must be present and exact); an optional one is pinned only when present (may be omitted, must never be contradicted).
- **Pinned values** (`literalValue` on a field) — `type="admin"` on a successful promote, `matched="true"`/`"false"` on a blocklist update, `code=429` on a rate-limit error. `parserRequired` separates the two forms: a required pin is a hard discriminator (must be present and exact); an optional one is pinned only when present (may be omitted, must never be contradicted).
- **The per-RPC error vocabulary** (`errorArms`, plus `errorEnvelope` for a two-level error) — a **closed** set, and it differs per RPC: `BatchGetGroupInfo` accepts `400 bad-request` and `429 rate-overlimit` and **rejects `404 item-not-found`**, even though that mixin exists and other RPCs use it. Each arm pairs the `code` with the `text` that must accompany it, so an emitter cannot combine one arm's code with another's text and produce a stanza no branch accepts; an arm that range-checks instead of pinning carries `codeMin`/`codeMax`. The variant's `kind` says which side is at fault (`client_error` / `server_error`), derived from the codes rather than from the parser's name.
- **Response enums** (`enumRef` on a field) — the legal values behind an `attrStringEnum`, resolved the same way the request side already resolves them, instead of a bare `"type": "enum"`.
- **The accessor's decoded type**, kept faithful: every `attr*` / `content*` spelling WA's parsers use is classified (a `maybeAttrX` derives from `attrX`, so a flavour cannot be covered for one spelling and missed for the other), and the JID flavours stay distinct — a PN user JID and a LID user JID are different identities for the same person and must never collapse into one `string`.
- **Notification action unions** (`notifications[].actions`) — the payload inside the envelope. The `wireTag → actionType` mapping is **many-to-one** (`not_ephemeral` normalises into `ephemeral` with `duration: 0`, so branching on `not_ephemeral` is dead code) and field names are rebound (the disappearing-message timer arrives in `expiration`, but the action field is `duration`). Neither is derivable from the wire.
- **The out-of-set policy** (`unknownValue` on a field) — WA writes two things into one accessor name, and only the decoded type was ever published. `<meta polltype>` is read with `attrEnumOrNullIfUnknown` and `<message type>` with `attrEnum`: same `"type": "enum"`, same `parserRequired`, and opposite behaviour on a value the enum does not list — one yields null and the parse continues, the other rejects the stanza. That is the difference between an enum a consumer may close and one that needs a fallback variant, and it used to be recoverable only by reading the WhatsApp method name in English.
- **What `parserRequired` does not say.** It means the accessor is `attrX` rather than `maybeAttrX` — the parser rejects the node without the value *at the point it reads it*. It is not "the wire always carries this": `polltype` is `parserRequired` and the client only reads it when the envelope's `type` is `poll`, 1 of 7 values. No domain models the branch condition, so the field is an upper bound on presence; a consumer that validated on the old name (`required`) rejected legitimate traffic.
- **One index for every enum** — `enums/index.json` is keyed by `(module, name)` and contains every enum any `enumRef` in the IR names, whether or not it is an `$InternalEnum`. `name` is **not** a key: `ENUM_FALSE_TRUE` is defined by eleven different modules, and `EventType`'s two definitions disagree on `valueKind`. Those generated names are flagged `syntheticName` — WA spells them out of their own members, so they are not type names. `bitPosition` marks an int enum whose values are shift distances (`1 << 2`, not `2`), recovered from the bundle shifting by a member.
- **What the extractor could not resolve says so.** `target` separates the group server (`g.us`) from one group's own JID (`group_jid`) — WA writes them from differently-named mixins and 26 of the 33 `w:g2` requests take the second — and says `unknown` when a `to` resolves to no fixed server, `unset` when nothing writes one, instead of all four reading `s.whatsapp.net`; a field with no accessor whose content is its children is `type: "node"` instead of `string`. Both counted in the manifest, both guarded.

Note the contract version: this raised `schemaVersion` to **2.0.0**, and it is a real major bump rather than a cautious one. Four changes need action from a 1.x consumer:

Expand All @@ -38,7 +42,15 @@ 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.
**`schemaVersion` is now 3.0.0.** That release turned the same rule on this repository's own output: three fields were asserting things the extractor had not established, and each is fixed by widening or renaming an existing field rather than by adding an optional one. Three changes need action from a 2.x consumer:

- `IqTarget` gained `group_jid`, `unset` and `unknown`, widening the value space of `request.target` / `target`. All 143 stanzas used to read `s.whatsapp.net` — the enum had nowhere to put "not resolved", and the rule that filled it keyed on a literal `to="g.us"` the builders had stopped writing. They now read 106 `s.whatsapp.net`, **26 `group_jid`** and **6 `g.us`** — the `w:g2` requests, which the IR addressed to the server — and 5 `unknown`: the four newsletter requests whose `to` is `WAWap.JID(newsletterId)`, plus `GetGroupProfilePictures`, which folds in a runtime router addressing either a group's JID or the group server. Migration: match the three new values. `g.us` is unchanged on the wire and still means the literal group server (create, leave, list); what moved out of it is `group_jid`, one group's own `<group>@g.us`, which you must supply — sending the bare server there answers nothing. `unknown` means the addressee is a parameter of the call and the IR does not know it; `unset` that nothing writes a `to` at all and neither should you. A closed-enum consumer rejects the document until it handles them.
- `ParsedFieldType` gained `node`, and 617 fields that declared `string` while carrying children now declare it — `wAMOSubMixin`, `groupAddressingModeMixin` and the other folded-in payload mixins. Migration: a `node` is a container, not a value; read its `children` and generate no scalar for it. A consumer switching on `type` gets a value it has no arm for, which is better than the arm it had.
- A response field's `required` is now **`parserRequired`**. A rename rather than a doc fix, because the old name stated a wire fact the field never carried (see the note above). Migration: rename the key. Reading the old one yields `undefined`, which fails loudly instead of defaulting to "optional".

Additive in 3.0.0, so a 2.x consumer can ignore them: `unknownValue` on enum-accessor fields; `syntheticName` / `bitPosition` in the enum catalog; and the catalog itself growing from 328 to 403 entries so every `enumRef` resolves against it — 75 of the 87 referenced `(module, name)` pairs were in no catalog before.

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.targets.resolved` 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. The unresolved states are guarded the other way — `scripts/lint-ir.py` pins the count of unaddressed requests and of unjudged accessors to an exact baseline. A rise means a constraint is being lost; a fall means extraction improved and the baseline owes an update. Either way the lint fails, so neither direction passes unnoticed.

## Quick start

Expand Down
Loading
Loading