Skip to content

ir: check the IR is usable, recover the enums WA composes, and pin the bootloader map - #37

Merged
jlucaso1 merged 46 commits into
mainfrom
ir-followups
Jul 29, 2026
Merged

ir: check the IR is usable, recover the enums WA composes, and pin the bootloader map#37
jlucaso1 merged 46 commits into
mainfrom
ir-followups

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The follow-ups the constraints PR (#36) left on the record, minus the two that stopped making sense.

What each one does

IR consistency in CI (scripts/lint-ir.py). The JSON Schemas validate shape: a field has the keys it should, of the right types. They cannot say whether the document is usable — whether a field the IR calls an enum names its legal values, whether a byte pin is the length it claims, whether a union carries the alternatives that make it one. Those are the questions a consumer writing a generator has to answer, and nothing checked them.

Two kinds of finding. An internal contradiction always fails: a literalValue on an integer field that is not an integer, a byteMin above its byteMax, an enumRef with no variants. A counted state — an enum WA composes at runtime and the extractor cannot name — is held to a baseline, so it may shrink but a rise fails, because that is a constraint starting to slip through unnoticed.

Verified by sabotage, not by reading: injecting one of each contradiction into a copy of the artifact produces five errors plus a baseline regression and exit 1; the real artifact exits 0.

Composed enums (babelHelpers.extends({}, VISIBILITY, {error: "error"})). This is how WA builds an enum out of another one — every privacy setting has a …_WITH_ERROR twin carrying the sentinel the parser checks for. The resolver knew object literals and $InternalEnum calls and nothing else, so those exports resolved to nothing and every field validating against one shipped as "type": "enum" with no values. It was the largest single entry in dropsByReason.

before after
iq.dropsByReason (enum) 41 33
fieldEnumRefs 151 167
enum fields with no values 173 155

VISIBILITY_WITH_ERROR now reads all, contacts, contact_blacklist, none, error.

The three action findings left open, plus the reason they were left open. An accessor passed through an alias (var x = attrString("jid"); h(x) — the minifier hoists nearly every read into a local, so the helper was handed the text x); a mapped-child callback passed by name (mapChildrenWithTag("participant", parseP) emitted the child with an empty field list); and a collection only one branch carries (NotifActionChild gains required, weakened when a branch producing the same action lacks it — the same all-branches accounting required already does for a scalar, one level up).

And a # What this models, and what it does not section on actions.rs. Reading those arms means interpreting minified JavaScript, and that file has grown a small scope-and-control-flow analysis to do it; six review rounds were spent discovering its edges one construct at a time. Writing the boundary down turns the next such finding from a surprise into a decision, and records the invariant that matters if it grows: a reader that guesses is worse than one that reports a gap.

The bootloader extraction, pinned. The JS never carries a wasm URL — it asks for a numeric bx id, and only the page's resource map turns that into something fetchable. Nothing recorded that map, so a change in the bootloader surfaced only as a wasm count that quietly stopped growing. wasmResources cannot cover it: it counts every handle, most addressing theme images that come and go, so it is deliberately unguarded. wasm.lock.json now carries the resolved id→URI map, the handles the page inlined, and the requests sent and failed.

Pinned is the extraction, not the page: the HTML carries nonces and timestamps and is byte-unstable by construction, so hashing it would fail the determinism gate for reasons unrelated to the protocol.

Two follow-ups deliberately dropped

Compile the generated Rust in CI, and enforce the constraints in the reference codegen. Both were aimed at an artifact on its way out: the committed contract is the IR and its schemas, and a consuming library implements the generator in its own language. Work spent hardening the Rust output would have been spent on the part being removed. The IR consistency check above is the language-neutral replacement for the first; the second has no replacement because, once the codegen goes, it has no subject.

The mex docId history was built and then dropped as out of scope for this change.

Validation

  • cargo fmt --all --check; clippy --workspace --all-targets and --all-features, both -D warnings
  • cargo test --workspace582 tests, 11 added here
  • scripts/validate-schemas.py generated → 12/12; scripts/lint-ir.py generated → consistent
  • determinism: update --bundles <pinned> --check27 committed artifact(s) up to date
  • each behavioural fix reverted on its own to watch its test fail

Summary by cubic

Adds a CI linter to enforce IR usability, resolves WA‑composed enums, pins bootloader handle→URL mappings, and improves notification action extraction. Also ratchets duplicate‑key flattening per extra entry and keys it by semantic path so substitutions are visible without breaking CI.

  • New Features

    • IR linter: verify accessor↔type compatibility as a set per accessor (e.g., attrInt may back integer, timestamp, timestamp_millis).
    • IR linter: require byteLength only on accessors that have one; allow request‑side constBytes to carry lengths with no accessor.
    • IR linter: track flattened discriminated shapes (duplicate‑key constraints) as a ratcheted baseline, counted per extra entry and keyed by semantic path.
  • Bug Fixes

    • Actions: reconcile mapped‑child wire tags across both ternary branches; equal tags resolve, differing tags refuse. Reject same‑tag branches with different callbacks.
    • Enums: detect mutating extends(...) in any call position; refuse conflicting export writes and mismatched deferred aliases vs inline bodies.
    • Scoping: honor the temporal dead zone for let/const in for-in/for-of headers when resolving reads.
    • Linter: require each *Source to be paired with its value and non‑empty; report standalone sources.

Written for commit e332de1. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Notification action payloads now include a required flag describing whether repeated-collection data is present on every producing branch.
    • wasm.lock.json can optionally store bootloader pin metadata and preserve/update it during --wasm runs.
  • Bug Fixes
    • Improved notification action-union extraction/inlining and requiredness inference, with tighter guard/optionality and more reliable callback/mapped-child resolution.
    • Enum export resolution now composes extends with safer merging and stricter shadowing/scope handling.
  • Validation
    • CI now runs a deterministic generated-IR lint pass with ratcheted consistency/drift checks.

jlucaso1 added 5 commits July 28, 2026 12:22
The JSON Schemas validate shape: that a field has the keys it should, of the
right types. They cannot say whether the document is USABLE — whether a field
the IR calls an enum names its legal values, whether a byte pin is the length it
claims, whether a union carries the alternatives that make it one. Those are the
questions a consumer writing a generator has to answer, and nothing checked them.

This replaces the "compile the generated Rust in CI" follow-up. That guard was
aimed at an artifact that is on its way out — the committed contract is the IR
and its schemas, and a consuming library implements the generator in its own
language. A guard on the Rust output would have been work spent on the part that
is being removed; this one is language-neutral and sits on what survives.

Two kinds of finding. An internal contradiction (a `literalValue` on an integer
field that is not an integer, a `byteMin` above its `byteMax`, an `enumRef` with
no variants) always fails: the document is saying two things at once. A counted
state (an enum WA composes at runtime and the extractor cannot name) is held to a
baseline, so it may shrink but a rise fails — that is a constraint starting to
slip through unnoticed.

Verified by sabotage rather than by reading: injecting one of each contradiction
into a copy of the artifact produces five errors plus one baseline regression and
exit 1, and the real artifact exits 0.

One thing the writing of it taught: keying "is this a field?" on `method`/
`wireName` looked right and silently skipped appstate, which carries neither —
the linter reported a clean count while missing a domain. It keys on the type
vocabulary now.
`babelHelpers.extends({}, VISIBILITY, {error: "error"})` is how WA builds an enum out of
another one — every privacy setting has a `…_WITH_ERROR` twin carrying the sentinel the
parser actually checks for. The resolver recognised object literals and `$InternalEnum`
calls and nothing else, so those exports resolved to nothing and every field validating
against one shipped as `"type": "enum"` with no values. It was the largest single entry
in `dropsByReason`.

The merge is left-to-right with later keys overriding, as the runtime does, and each
operand must be something already understood: a literal, an `$InternalEnum`, or a local
this pass has already bound. Anything else refuses the WHOLE composition — a partial
merge is a closed value set missing members the runtime accepts, which is worse than
recording the loss.

  iq dropsByReason  41 -> 33
  fieldEnumRefs    151 -> 167
  enum fields with no values (lint)  173 -> 157

VISIBILITY_WITH_ERROR now reads `all, contacts, contact_blacklist, none, error`, and the
three other privacy compositions resolve alongside it.

The empty `{}` first operand needed its own case: `parse_enum` rejects it for having no
variants, so it fell through to the refusal path and killed every merge before it started.
The first version of this shipped with that bug and recovered nothing — the counters said
so, which is why the linter's counted states are worth having.
… boundary

The three findings left open at the end of the review, plus the thing that made them
worth leaving open: nothing said what the action reader models.

* An accessor passed through an ALIAS. The minifier hoists nearly every read into a
  local, so `var x = attrString("jid"); h(x)` handed the helper the text `x` — no wire
  read in it, and meaningless inside the helper's own parse. Arguments are resolved
  through the caller's scope before the binding decides, so the alias form works.
* A mapped-child callback passed BY NAME. `mapChildrenWithTag("participant", parseP)` is
  a function too; rejecting the identifier before looking at its body emitted the child
  with an empty field list, which tells a consumer the element carries nothing.
* A collection only one branch carries. `NotifActionChild` gains `required`, weakened
  when a branch producing the same action lacks it — the same all-branches accounting
  `required` already does for a scalar, one level up. Serialized only when false, so the
  common case adds nothing to the document.

And a `# What this models, and what it does not` section on the module. Reading these
arms means interpreting minified JavaScript, and this file has grown a small
scope-and-control-flow analysis to do it; six review rounds were spent discovering its
edges one construct at a time. Writing the boundary down is what turns the next such
finding from a surprise into a decision — and it records the invariant that matters if
this grows: a reader that guesses is worse than one that reports a gap.

576 tests; each of the three reverted alone to watch its test fail.

The named-callback path shipped broken on the first attempt: the `(…)` that makes a
declaration parse as an expression also hides it behind a `ParenthesizedExpression`, so
the function gate rejected it and the child came back empty. Second time that wrapper has
cost a round.
The JS never carries a wasm URL — it asks the bootloader for a numeric `bx` id, and only
the page's resource map turns that id into something fetchable. Nothing recorded that map,
so a change in the bootloader's shape surfaced only as a wasm count that quietly stopped
growing. `wasmResources` cannot cover it either: it counts every handle, most of which
address theme images that come and go with UI work, so it is deliberately unguarded.

`wasm.lock.json` gains the extraction: the resolved id→URI map, how many handles the page
inlined before any request, and how many endpoint requests were sent and failed. A run
that suddenly needs more rounds, or starts failing, is the early signal that the endpoint
contract moved — and the map itself can be diffed across releases.

What is pinned is the EXTRACTION, not the page. The HTML carries nonces and timestamps and
is byte-unstable by construction; hashing it would fail the determinism gate for reasons
that have nothing to do with the protocol.

The key is absent, not empty, when a run never asked the bootloader anything — a local
`--bundles` regen must not be readable as "the map is empty now", and the lock is written
only by the authoritative fetch path in the first place.

576 tests, determinism check clean, IR consistent.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR updates notification requiredness inference, persists bootloader pins in version-aware wasm locks, improves composed enum resolution across JavaScript scopes, and adds deterministic generated-IR invariant validation to CI.

Changes

Notification action extraction

Layer / File(s) Summary
Required child metadata
crates/wa-ir/..., crates/wa-codegen/src/notif_export.rs, crates/wa-notif/src/actions.rs
NotifActionChild.required defaults to true, branch folding marks incomplete children optional, and generated action tables emit the field.
Helper and mapped callback resolution
crates/wa-notif/src/actions.rs
Helper expansion preserves nested source coordinates, resolves callbacks through caller scope, evaluates absence-capable guards, and collects named callback fields.
Edge-case extraction coverage
crates/wa-notif/src/tests.rs
Fixtures and tests cover membership branches, aliases, named callbacks, shadowing, cycles, helper delegation, and optional children.

Bootloader pin persistence

Layer / File(s) Summary
Bootloader lock schema
crates/whatspec/src/lock.rs, crates/whatspec/src/restore.rs
WasmLock gains optional serialized BootloaderPins metadata and a constructor preserving deterministic wasm ordering and hashes.
Resolution and version-scoped cache data
crates/wa-fetch/...
Resolution records observed ids and failed requests, while cached wasm handles are scoped to the requested version.
Pin propagation and lock updates
crates/whatspec/src/main.rs
Bootloader pins propagate through fetch paths, empty-payload updates, same-version merging, and persistence tests.

Composed enum resolution

Layer / File(s) Summary
Extends enum resolver
crates/wa-enums/src/lib.rs, crates/wa-enums/Cargo.toml
babelHelpers.extends(...) compositions merge ordered enum operands while scope tracking rejects rebound, incompatible, empty, or unreadable inputs.

Generated IR validation

Layer / File(s) Summary
IR invariant linter
scripts/lint-ir.py
The linter traverses generated documents, checks structural and domain invariants, tracks pinned states, and returns failure status for violations.
CI validation step
.github/workflows/ci.yml
CI runs the linter against committed generated artifacts.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WasmResolution
  participant load_wasm
  participant update
  participant WasmLock
  WasmResolution->>load_wasm: return payloads, observed ids, and diagnostics
  load_wasm->>update: provide bootloader pins and live handle ids
  update->>WasmLock: merge same-version pins and write lock metadata
Loading

Possibly related PRs

Poem

A rabbit hops through branches bright,
Pins and enums fall into line.
Optional leaves now know their place,
IR guards every generated space.
Hop, hop—the metadata shines!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the three main change areas: IR usability checks, composed enum recovery, and bootloader pinning.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/whatspec/src/main.rs (1)

352-358: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Persist pins when extraction yields no payloads.

load_wasm returns Some(pins) for an empty payload set (Line 1310), but this payload-only guard skips WasmLock::with_bootloader. A blocked or changed bootloader therefore leaves no pinned request/failure/map outcome—exactly the diagnostic state this PR introduces. Preserve the existing payload safety policy while updating compatible lock metadata, or write an explicit empty extraction record; do not silently discard boot_pins.

🤖 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/whatspec/src/main.rs` around lines 352 - 358, The authoritative WASM
lock update currently runs only when `wasm` is non-empty, silently discarding
`boot_pins` for empty extractions. Update the flow around
`WasmLock::with_bootloader` so compatible lock metadata is persisted when
`load_wasm` returns pins with no payloads, while preserving the existing payload
safety policy; alternatively write the explicit empty extraction record rather
than dropping the pins.
crates/wa-notif/src/actions.rs (1)

786-810: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve optionality when merging an already-folded child.

Line 797 merges same-tag children but ignores c.required. If from already represents branches where the child is absent, existing.required can incorrectly remain true. Combine both states with existing.required &= c.required.

Proposed fix
 Some(existing) if existing.wire_tag == c.wire_tag => {
+    existing.required &= c.required;
     let dead = self.dead_child_fields.entry(c.name.clone()).or_default();
     merge_fields(&mut existing.fields, c.fields, false, dead);
 }
🤖 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-notif/src/actions.rs` around lines 786 - 810, Update the same-tag
child merge arm in the surrounding merge function so it combines requiredness
before merging fields: set existing.required to the conjunction of its current
value and c.required. Keep the existing merge_fields and dead_child_fields
handling unchanged.
🤖 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 `@scripts/lint-ir.py`:
- Around line 137-138: Update the field traversal around check_field so
field-shaped objects are identified independently of membership in FIELD_TYPES.
Ensure every such object reaches validation and its type is explicitly reported
as an error when not in FIELD_TYPES, preserving normal validation for recognized
field types.

---

Outside diff comments:
In `@crates/wa-notif/src/actions.rs`:
- Around line 786-810: Update the same-tag child merge arm in the surrounding
merge function so it combines requiredness before merging fields: set
existing.required to the conjunction of its current value and c.required. Keep
the existing merge_fields and dead_child_fields handling unchanged.

In `@crates/whatspec/src/main.rs`:
- Around line 352-358: The authoritative WASM lock update currently runs only
when `wasm` is non-empty, silently discarding `boot_pins` for empty extractions.
Update the flow around `WasmLock::with_bootloader` so compatible lock metadata
is persisted when `load_wasm` returns pins with no payloads, while preserving
the existing payload safety policy; alternatively write the explicit empty
extraction record rather than dropping the pins.
🪄 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: 8c092588-3931-4d62-a656-f8162ea521b6

📥 Commits

Reviewing files that changed from the base of the PR and between 96a69fb and 861094c.

⛔ Files ignored due to path filters (3)
  • generated/iq/index.json is excluded by !**/generated/**
  • generated/manifest.json is excluded by !**/generated/**
  • generated/schema/notif.schema.json is excluded by !**/generated/**
📒 Files selected for processing (11)
  • .github/workflows/ci.yml
  • crates/wa-codegen/src/notif_export.rs
  • crates/wa-enums/src/lib.rs
  • crates/wa-ir/src/lib.rs
  • crates/wa-ir/src/notif.rs
  • crates/wa-notif/src/actions.rs
  • crates/wa-notif/src/tests.rs
  • crates/whatspec/src/lock.rs
  • crates/whatspec/src/main.rs
  • crates/whatspec/src/restore.rs
  • scripts/lint-ir.py

Comment thread scripts/lint-ir.py Outdated
…ld child optionality

Three findings on the follow-ups themselves.

The linter's docstring said an unrecognized `type` was worth reporting and the code used
the vocabulary as its DETECTOR, so a typo'd or newly-added type silently excused the field
from every other check — the doc promising what the code did not do, which is the class
this whole guard exists to catch. A field is now anything with a known type OR anything
carrying a key only a response field has, and the second clause reports the type. The
marker only counts when the object has a `type` at all: assertions carry `referencePath`
too, and the first version of this flagged four of them as untyped fields.

`load_wasm` returns pins even when it resolved no payloads — a blocked or changed
bootloader is exactly the state the pins were added to record — but the lock write is
guarded on a non-empty payload set, so that state was the one case where they were
dropped. An empty run now rewrites only the bootloader section, leaving the recorded
payload set alone, which is what the emptiness guard is actually protecting.

And merging a same-tag child ignored the incoming `required`. `from` may itself be a fold
of several branches that already knows the child is absent from one, so keeping
`existing.required` re-asserted a presence the incoming side had disproved.

577 tests, determinism clean, IR consistent. The unknown-type check verified by renaming
one `type` in a copy of the artifact: one error, exit 1.
@jlucaso1

Copy link
Copy Markdown
Contributor Author

Os dois achados fora do diff também estão aplicados em d8eee2d — respondo aqui já que não têm thread.

main.rs:352-358 — pins descartados quando não há payloads. Achado certeiro, e o pior tipo: era o único caso em que a feature se perdia, e é justamente o caso que ela existe para registrar. Um bootloader bloqueado ou alterado é exatamente o estado que rende zero payloads, então a guarda de vazio o engolia.

Agora uma execução sem payloads reescreve a seção do bootloader, deixando intacto o conjunto de payloads gravado — que é o que a guarda de vazio de fato protege. Lock ausente ou ilegível fica como está: não há conjunto a preservar, e inventar um a partir de uma execução vazia é o que a guarda proíbe.

actions.rs:786-810 — opcionalidade perdida ao fundir filho já dobrado. Também certeiro. O from pode ser ele mesmo o fold de vários ramos e já saber que o filho falta em um deles; fundir só os campos mantinha existing.required verdadeiro e reafirmava uma presença que o lado que chegava já tinha desmentido. Agora é existing.required &= c.required.

577 testes, determinismo em dia, IR consistente, clippy --all-features limpo.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 861094c61a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-notif/src/actions.rs
Comment thread crates/whatspec/src/main.rs
Comment thread scripts/lint-ir.py Outdated
Comment thread crates/whatspec/src/main.rs Outdated
Comment thread crates/whatspec/src/main.rs Outdated
Comment thread crates/wa-enums/src/lib.rs
Comment thread crates/wa-ir/src/notif.rs Outdated
Comment thread scripts/lint-ir.py Outdated
Comment thread scripts/lint-ir.py Outdated
jlucaso1 added 2 commits July 28, 2026 13:10
… pin defects

Seven findings on the follow-ups. Two others in the same review were already fixed in
d8eee2d, which the review predates.

The linter accepted a DECREASE silently, which banks the difference as slack: 157 → 150
followed by seven newly unresolved enums is back at 157 and passes — precisely the drift
the counted states exist to catch. It is a ratchet now; an improvement is real work and
updating the number with it costs one line.

Two invariants the linter claimed and did not check, both because they were gated on the
`ParsedFieldType` vocabulary. Request-side and stanza attributes carry `enumRef` keyed by
`kind`, so the pending marker — an empty `variants`, which tells a consumer the attribute
admits NO value — could ship on those surfaces; and request content states the same byte
pin as `constBytes` that a response states as `literalValue`, so neither malformed hex nor
a length mismatch was checked there. Both are checked on every dictionary now.

`babelHelpers.extends(base, {B: "b"})` MUTATES `base`. Recovering only the result would
publish a `BASE` export missing a member the runtime has — an enum claiming to reject a
value it accepts, which is the worst thing this can produce. WA's convention is an empty
target, so requiring one costs nothing and rules the mutation out.

Three on the bootloader pins:

* they were built from this run's sample while the payload set accumulates from the cache,
  so a later successful run could DELETE ids an earlier one resolved while their payloads
  stayed — the supposedly diffable map differing between two runs of one release. Built
  from the accumulated handle map now.
* `failures` also carries pre-request diagnostics (no endpoint parameters, no deferred
  components), so a `failedRequests` counted from its length contradicted its own name.
  `WasmResolution` counts request failures separately.
* `NotifActionChild::required` skipped its `true` case, leaving a non-Rust consumer — who
  the IR is for — unable to tell "required by default" from "unspecified". Always
  serialized, and the schema now carries `default: true`.

577 tests, 12/12 schemas, determinism clean, IR consistent.
…ves behind

`update_lock_bootloader` is `#[cfg(feature = "fetch")]` and its call site was not, so
`cargo build -p whatspec --no-default-features` — a job this repo runs precisely because
the tool must build without the network stack — stopped compiling. `cargo test
--workspace` never exercises that configuration, and I never ran it.

The `BTreeMap` import moves behind the same gate for the same reason: only the fetch path
builds the pin map from it.

Checked against the whole CI matrix this time rather than the subset I had been running:
fmt, clippy (default and `--all-features`), the workspace tests, the no-default-features
build, `wa-fetch` and `wa-ir` on wasm32, `wa-ir --features schema`, both artifact scripts,
and the determinism gate. The two remaining no-fetch warnings are pre-existing on main,
which now emits three of them.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb3de9f35b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/whatspec/src/main.rs Outdated
Comment thread crates/whatspec/src/main.rs
Comment thread scripts/lint-ir.py
Comment thread crates/wa-enums/src/lib.rs Outdated
Comment thread crates/wa-notif/src/actions.rs
Comment thread scripts/lint-ir.py Outdated
…enum name

Six findings from the Codex round on bb3de9f. Five are latent — they change no
committed artifact today — and one changes the IR linter's count.

- `BootloaderPins::wasm_handles` promised wasm and delivered the page's whole
  `bxData`: images and every other resource, which churn far more often than the
  wasm set and so swamped the diff signal the field exists for. Filtered with the
  resolver's own `is_wasm_url` rather than a second copy of the predicate.
- An empty `--wasm` run replaced the bootloader section wholesale, deleting ids
  earlier runs had resolved from the endpoint while keeping the payloads they
  resolved. The update workflow passes no `--cache`, so the lock is the only
  record on that path. The map now accumulates; the diagnostics still describe
  the current run.
- A mapped child behind a guard was always `required: true`, though `strip_guard`
  reaches through the guard to find it. Deriving that from `is_guarded` alone
  would be wrong in the other direction — the one live case,
  `t.hasChildren() ? t.mapChildrenWithTag(…) : [{wid: …}]`, always yields a
  collection — so only a literal absence on the far side makes it optional.
- `NamedResolver::locals` is flat while JavaScript is lexically scoped, so a name
  rebound inside a nested function could feed a composition the wrong body and
  publish a closed set naming members the runtime never accepts. No bundle in the
  pinned set has a single collision; both readers now refuse the name rather than
  guess.
- The linter counted appstate's two `protoEnum` fields as enums with no values.
  They name their values, just by reference — and with the baseline a ratchet, a
  new proto-backed enum could have cancelled out a real regression. Baseline
  157 -> 155.
- `literalValue` and `referencePath` are documented as mutually exclusive but were
  only ever checked apart. No live case, which is the point of adding it now.

Every fix has a test that fails without it, and the two that could plausibly have
been over-corrected are pinned from both sides.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04fbd20314

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/whatspec/src/main.rs Outdated
Comment thread scripts/lint-ir.py Outdated
…num links

Two findings from the Codex round on 04fbd20.

- On a version bump that resolved no payloads, the previous commit wrote this
  run's handles into a lock still stamped for the older release — and since that
  map now accumulates, it interleaved two releases' ids in a record whose whole
  purpose is being diffable. `warn_if_wasm_lock_is_now_stale` already detected
  the mismatch and only warned. The updater now takes the version and leaves a
  lock belonging to another one alone, saying so.
- WAM's 1431 enum-typed fields name a module and expect the consumer to find it
  in the document's own `enums` catalog, but the linter only ever inspected
  inline `enumRef` objects. A dangling module — or a definition with no variants,
  which the schema permits — read as internally consistent while a consumer could
  not encode the field. All 1431 resolve today; the check is keyed on the
  document having a catalog rather than on the name "wam", so a second domain
  adopting the shape is covered without anyone remembering.

Both have a test that fails without the fix; the linter's three failure modes
(dangling, empty, ambiguous) are each verified against a mutated document.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b199dd2407

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/whatspec/src/main.rs
The previous commit fixed the shrink only where a run resolved nothing. A run
that resolves a smaller endpoint subset also succeeds, and rebuilding the lock
from its own sample deletes ids an earlier run resolved — the same defect,
reached by a productive run rather than a blocked one. Without `--cache`, which
is how the update workflow runs, the lock is the only record, so nothing else
restores them.

Both paths now share one rule, version-scoped as before: `merge_prior_handles`
for the in-place edit, `pins_with_prior` for the paths that build a whole new
lock. The ids are provenance and never enter `wasmSetHash`, so keeping one whose
payload this run did not re-resolve costs nothing, while dropping it makes an
unchanged release diff against itself.

`pins_with_prior` is deliberately not feature-gated: its caller is on the
ordinary write path and compiles in every configuration.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1fb4c8792

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-enums/src/lib.rs Outdated
Comment thread crates/wa-notif/src/actions.rs
Comment thread crates/wa-notif/src/actions.rs Outdated
Comment thread scripts/lint-ir.py Outdated
Comment thread scripts/lint-ir.py
Comment thread scripts/lint-ir.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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-notif/src/actions.rs`:
- Around line 1984-1993: Update guard_admits_absence to unwrap
ParenthesizedExpression nodes before testing optionality, then apply the
existing conditional and logical-expression checks to the unwrapped expression.
Preserve the current ternary and && behavior, and add coverage for parenthesized
ternary and && guards.

In `@crates/whatspec/src/main.rs`:
- Around line 2189-2213: Update pins_with_prior to distinguish an absent prior
lock from a present lock that cannot be read or parsed: log the read or
deserialization failure using the existing logging approach, while preserving
the current same-wa_version filtering and no-prior-lock behavior. Keep the
function’s return contract and merge_prior_handles flow unchanged for valid
locks.
🪄 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: ec70f97b-2838-4618-b675-3bdd54b30c56

📥 Commits

Reviewing files that changed from the base of the PR and between 861094c and d1fb4c8.

⛔ Files ignored due to path filters (3)
  • generated/manifest.json is excluded by !**/generated/**
  • generated/notif/index.json is excluded by !**/generated/**
  • generated/schema/notif.schema.json is excluded by !**/generated/**
📒 Files selected for processing (9)
  • crates/wa-enums/src/lib.rs
  • crates/wa-fetch/src/bootloader.rs
  • crates/wa-fetch/src/discover.rs
  • crates/wa-fetch/src/lib.rs
  • crates/wa-ir/src/notif.rs
  • crates/wa-notif/src/actions.rs
  • crates/wa-notif/src/tests.rs
  • crates/whatspec/src/main.rs
  • scripts/lint-ir.py

Comment thread crates/wa-notif/src/actions.rs
Comment thread crates/whatspec/src/main.rs
Six from Codex on d1fb4c8 plus two from CodeRabbit. Every one is latent — no
committed artifact changes.

Guards, continuing the mapped-child fix:

- `guard_admits_absence` matched on the expression directly, so a parenthesized
  guard answered "no guard here". `strip_guard` unwraps the same wrapper, and the
  two have to agree on what they are looking at; that wrapper has cost this file
  a field twice before.
- A guard around a value-position HELPER was lost entirely: the inliner is handed
  `strip_guard(value)`, so the map inside looks unguarded. The guard belongs to
  the key, not the helper, and is now applied to what the helper yields.

Scope:

- Only `var`-form rebindings reached `shadowed`; a bare `e = {…}` never did, so a
  module reassigning an operand before composing it still published the stale
  body — the same wrong closed set through the one write form the guard did not
  watch. Rebound to something unreadable refuses the name too.
- A named mapped-callback was resolved straight from the module's function table,
  ignoring the caller's bindings. A parameter of the same name shadows it at
  runtime, so an unrelated function's fields could be published as the element's.

Linter:

- Half a numeric range (`intMin` with no `intMax`) passed, because the check
  required both keys before looking at either.
- `generated/enums/` is a catalog nothing references, so validating references
  alone left all 326 definitions unexamined.
- `kind: "const"` with no `value` tells an emitter the value is fixed and then
  declines to say what it is. Only this direction: `kind` is shared with the
  assertion vocabulary, where `attr`/`content` carry a `value` legitimately, so
  the converse check would have flagged 1781 correct nodes.
- `pins_with_prior` treated an unparseable lock as an absent one, silently
  dropping the handles it was accumulating to preserve.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 031a442a67

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/lint-ir.py Outdated
Comment thread crates/wa-notif/src/actions.rs
Comment thread scripts/lint-ir.py
Comment thread crates/wa-enums/src/lib.rs
…t an empty catalog hides

Four from Codex on 031a442. One is a crash; the rest are latent.

- The callback-alias resolution added last round could not terminate.
  `deref_ident` caps at four hops, so on a cycle whose length does not divide four
  (`a → b → c → a`) it hands back a *different* identifier every call, and
  recursing on that walks the cycle until the stack goes. Reverting the guard
  makes the new test abort with a stack overflow, so this is a real crash on a
  shape the extractor could meet, not a theoretical one. It now recurses only into
  something that is no longer an identifier; an unresolved chain is refused, which
  is what the hop limit was for.

- Function parameters bind their name for the whole body, which neither the
  declarator nor the assignment path saw, so `function f(e){ extends({}, e, …) }`
  still composed over the outer `e`. Refusing such names module-wide — the cheap
  version — was measured against the pinned bundles and LOST a real constraint
  (`STANZA_MSG_TYPES`), so this tracks a scope stack instead and refuses only
  inside the body that binds the name. `generated/` is unchanged either way, which
  is the point: the blunt version was not.

- `check_enum_catalog_refs` returned early on an EMPTY catalog, which is the worst
  case rather than a trivial one: if extraction collapsed the list while the
  enum-typed fields remained, all 1431 references are dangling and the document
  was still reported consistent. An empty list is an empty lookup.

- A WAM event's `code` is its wire identifier and nothing checked it was unique.
  A consumer building a dispatch table by code would silently overwrite one of a
  colliding pair, leaving one event shape unreachable.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57a96933f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-enums/src/lib.rs Outdated
Comment thread crates/wa-enums/src/lib.rs
Comment thread crates/whatspec/src/main.rs Outdated
Comment thread scripts/lint-ir.py Outdated
Comment thread scripts/lint-ir.py
Comment thread scripts/lint-ir.py
…ten three checks

Six from Codex on 57a9693. One is a real cross-release bug; the rest are latent.

- `BundleCache::wasm_handles` read its manifest with no version check while
  `wasm_payloads` refuses a stale one through `usable_manifest`. A cache left from
  the previous release could therefore feed its ids into this release's pins —
  the same cross-version mixing the lock path already refuses, reached by the one
  door left open. It now takes the version, like everything else that reads that
  manifest for a purpose.

Two holes in last round's parameter-scope fix, both found by pushing on it:

- An unreadable redeclaration (`var e = getEnum()` in a nested scope) shadows just
  as much as a readable one, and left the outer body in `locals` for a later
  composition to publish. Refused only when the name is already bound: an
  unparseable `var` that shadows nothing costs nothing to ignore.
- A deferred export alias (`function f(e){ i.OUT = e }`) is resolved AFTER the
  walk, when the parameter stack is already unwound, so the scope has to be
  recorded at the assignment rather than looked up later.

Linter:

- A union with two entries of the SAME name is not a two-alternative union: the
  name is the discriminator, and the Rust codegen emits it as the variant
  identifier.
- `valueKind` is what tells a consumer how to represent an enum's values, and the
  schema permits any `Scalar` per variant, so nothing checked the two agreed.
- A `content` assertion is the fixed text a marker variant matches on; without
  the value a consumer cannot tell when the variant applies. Kept narrow — an
  `attr` assertion may legitimately assert only presence.

`generated/` is unchanged by all six.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7fd371605e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-enums/src/lib.rs Outdated
Comment thread scripts/lint-ir.py
Comment thread crates/wa-notif/src/actions.rs
Comment thread scripts/lint-ir.py
…er pattern

Four from Codex on 7fd3716. One drops a field today; the rest are latent.

- Nested inlining re-parses a SYNTHETIC buffer, but `local_call_source` kept
  slicing the module source with spans belonging to it. The bounds check never
  fired — those offsets are in range — so the argument text was simply unrelated,
  and a value threaded through two helpers (`mk2(v){ return mk(v) }`) lost its
  field. `ArmCtx` now carries the source its spans index, and `inline_local`
  rebinds it to the buffer it parsed. `locals` became a borrow so the context is
  cheap to re-point; there is still exactly one construction of it.

- `param_names` saw only plainly-named parameters, so `function f({e})` and a rest
  element bound nothing as far as the shadow guard was concerned. Uses
  `get_binding_identifiers` — the same traversal `wa_scan::shadow_params` already
  uses for its own shadowing — rather than a second hand-rolled walk to drift
  from.

- A WAM field's `id` is its wire identifier within its event, and only the
  event-level `code` was checked. Scoped per event: ids repeat across events by
  design.

- `fields`, `constantFields` and `children` are three representations of one
  object-key namespace. Each is internally consistent by construction and nothing
  compared them, so an action could carry a wire-read `reason` and a constant
  `reason` at once — a shape runtime cannot produce.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7cfe9e56ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-notif/src/actions.rs
Comment thread crates/whatspec/src/main.rs Outdated
Comment thread scripts/lint-ir.py
Comment thread scripts/lint-ir.py
A header expression is evaluated before the construct's own binding exists, so it
resolves where the construct is written. The switch discriminant and the `for-in`
/`for-of` right-hand side were being visited under the pushed lexical scope, which
could only refuse a resolvable composition — safe, but the point of modelling
scopes is not to guess in either direction. A `for` init is left inside, being the
binding itself.

Shipped without a regression test, deliberately. The one written for it passed
with the fix REMOVED: reaching the header path needs the composition to be the
header expression and its result to be exported, and the resolver's supported
export forms make that awkward to express. A test verified to prove nothing is
worse than none, so this says so instead.

@jlucaso1 jlucaso1 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sobre os dois fora do diff:

Asserção de regressão para metadado de enum (tests.rs 636-638): válido, mas não aplicado nesta rodada pelo mesmo motivo que acabei de registrar no thread do lib.rs — acabei de descobrir que um teste que escrevi para o fix de header passava com o fix removido. Antes de adicionar mais asserções nessa área quero verificar cada uma contra o revert, e não tenho margem para fazer isso com cuidado agora. Fica anotado.

Ruff apontando complexidade em check_appstate_collections: procede — a função acumulou seis checagens distintas (collections, scopes, index literal, chatJidIndex, valueEnumFields, valueProtoType/valueField). Quebrá-la em funções por invariante é a limpeza certa, e é exatamente o tipo de refactor que prefiro fazer com o diff isolado em vez de no fim de uma sequência longa de correções sutis.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee948bc627

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-notif/src/actions.rs
Comment thread scripts/lint-ir.py
Comment thread scripts/lint-ir.py
`mapped_child` calls `strip_guard`, which always follows the consequent, so
`disabled ? 0 : node.mapChildrenWithTag(…)` — the same shape written the other
way round — dropped the child ENTIRELY. The absence check beside it already
inspects both branches, so the two disagreed about which shapes exist. Reversing
an equivalent ternary must not erase the collection.

Notification types and stanza tags become generated Rust identifiers through the
same `pascal_case` as collections, scopes and union alternatives, so two wire
spellings that normalise alike emit one variant twice.

Not applied: resolving each `valueEnumFields` KEY against its payload message's
fields. The pair check landed last round for `valueField`/`valueProtoType`;
extending it to the enum keys means walking nested field paths inside an
arbitrary message, which the brace-depth scanner does not do and which I would
rather build with its own measurement than bolt on. Recorded on the thread.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1987c5ae12

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-enums/src/lib.rs
Restricting exports to the factory's parameters compared SPELLING only, so a
nested `function f(i) { i.OUT = {…} }` — writing to its own parameter — was still
read as a module export and could publish a private object as a closed protocol
enum.

Frame 0 of the scope stack is the module factory itself, whose parameters ARE the
receivers, so a shadowing is any frame after it that carries the name. Checking
`param_shadows` directly would have refused every real export.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab74276317

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-enums/src/lib.rs
Comment thread crates/wa-enums/src/lib.rs Outdated
Comment thread crates/wa-notif/src/actions.rs Outdated
…nds call

Three from Codex on ab74276, each a gap in the fix that preceded it.

- The alternate-branch search matched on the expression directly, so
  `(cond ? 0 : map(…))` answered "not a conditional". Third time this wrapper has
  cost this file something; it is peeled up front now.
- The receiver-shadow check reads the scope stack, whose frames are filtered to
  names that collide with an ENUM local — and a receiver name is not an enum, so
  `function f(){ var i = {}; i.OUT = … }` walked straight through. The filter now
  keeps a name that collides with either.
- `extends(e, {B:"b"})` mutates `e` wherever it appears, and only a declarator's
  initializer was watched; a bare expression statement does it too.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

if can_escape(&d.body, true) {
collect_returns(&[&d.body], scope, out);
tombstone_branch_writes(s, scope);
} else {
collect_returns_into(&[&d.body], scope, out);

P2 Badge Treat continue as an early do-while path

When a do…while body conditionally continues before a write, such as var x = attr("jid"); do { if (c) continue; x = attr("lid") } while (false); return {id:x}, the return can observe either source. This certainty check calls can_escape, which detects break but not continue, so it takes the definite-write branch and publishes only lid; treat an unlabelled continue that can skip the remaining body as an early path here and tombstone the write.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-enums/src/lib.rs Outdated
Comment thread crates/wa-enums/src/lib.rs Outdated
Comment thread crates/wa-enums/src/lib.rs Outdated
…cting export

Three from Codex on 7c1d92c, each a place the preceding fix did not reach.

- The two-way receiver filter (enum local OR factory parameter) went into the
  function prescan and not the arrow one.
- `tmp = extends(e, {B:"b"})` is an expression statement wrapping an ASSIGNMENT,
  so reading the statement's expression found the assignment rather than the call
  that mutates.
- An export written twice with different bodies published whichever write came
  first. Which one a consumer sees is not something this pass can determine, so it
  now determines nothing; an identical repeat is still resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/wa-notif/src/actions.rs (1)

1942-1987: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Alternate-branch fallback is skipped whenever the consequent is any other call.

.filter(...) runs only on the final result of as_call(strip_guard(unwrapped)).or_else(...). If the consequent resolves to some call that isn't mapChildrenWithTag (e.g. cond ? otherCall() : t.mapChildrenWithTag(...)), as_call returns Some(other_call), so .or_else never fires (it only triggers on None), and .filter then discards the wrong call without ever looking at the alternate. The map in the alternate is silently dropped in exactly this shape. The currently tested cases (reversed_, parenRev) only exercise a non-call consequent (0), where as_call already returns None, so or_else happens to fire correctly — this masks the gap.

🐛 Proposed fix: filter each candidate before falling back
-    let call = as_call(strip_guard(unwrapped))
-        .or_else(|| match unwrapped {
-            Expression::ConditionalExpression(c) => as_call(strip_guard(&c.alternate)),
-            _ => None,
-        })
-        .filter(|c| callee_method(c) == Some(wap::MAP_CHILDREN_WITH_TAG))?;
+    let call = as_call(strip_guard(unwrapped))
+        .filter(|c| callee_method(c) == Some(wap::MAP_CHILDREN_WITH_TAG))
+        .or_else(|| match unwrapped {
+            Expression::ConditionalExpression(c) => as_call(strip_guard(&c.alternate))
+                .filter(|c| callee_method(c) == Some(wap::MAP_CHILDREN_WITH_TAG)),
+            _ => None,
+        })?;

Existing reversed_/parenRev behavior is preserved by this change (consequent literal still yields None from as_call, so the fallback still fires); it only adds correctness for the case where the consequent is a different call.

🤖 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-notif/src/actions.rs` around lines 1942 - 1987, Update
mapped_child’s call-selection logic so each branch candidate is validated
against MAP_CHILDREN_WITH_TAG before falling back to the alternate branch.
Ensure a non-matching consequent call does not prevent examining a matching
alternate call, while preserving the existing behavior for literal or
parenthesized consequents.
crates/wa-enums/src/lib.rs (1)

779-788: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mark non-declaration for-in/for-of left targets as shadowed too.

Both loop helpers only pass VariableDeclaration declarators into with_lexical; the _ => vec![] fallback also skips assignment-target forms (for (e in obj), for ({e,i} in obj), for ([e] of arr), etc.) and never marks colliding locals as shadowed. Extensional collection also ignores these forms. Like bare/destructured reassignment, these should be treated as rebinds that shadow enclosing locals before visiting the expression and body.

🤖 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-enums/src/lib.rs` around lines 779 - 788, Update
visit_for_in_statement and its for-of counterpart to collect binding names from
non-declaration assignment targets, including identifiers, object patterns, and
array patterns, instead of treating them as empty. Pass those names to
with_lexical so they shadow enclosing locals, while preserving RHS visitation
before entering the loop scope and collecting the same names for extension
handling.
🤖 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-notif/src/tests.rs`:
- Around line 980-987: Add an assertion for parenRev.required alongside the
existing parenRev.wire_tag assertion in the child-selection test, verifying that
the parenthesized reversed conditional produces the expected absent-path
requiredness, consistent with the reversed_ case.

In `@scripts/lint-ir.py`:
- Line 994: Reduce the complexity of check_appstate_collections by extracting
cohesive validations—such as collection-name collisions,
valueProtoType/valueField agreement, valueEnumFields, and chatJidIndex—into
focused helper functions, then have the main function delegate to them while
preserving its existing validation behavior and error reporting.

---

Outside diff comments:
In `@crates/wa-enums/src/lib.rs`:
- Around line 779-788: Update visit_for_in_statement and its for-of counterpart
to collect binding names from non-declaration assignment targets, including
identifiers, object patterns, and array patterns, instead of treating them as
empty. Pass those names to with_lexical so they shadow enclosing locals, while
preserving RHS visitation before entering the loop scope and collecting the same
names for extension handling.

In `@crates/wa-notif/src/actions.rs`:
- Around line 1942-1987: Update mapped_child’s call-selection logic so each
branch candidate is validated against MAP_CHILDREN_WITH_TAG before falling back
to the alternate branch. Ensure a non-matching consequent call does not prevent
examining a matching alternate call, while preserving the existing behavior for
literal or parenthesized consequents.
🪄 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: 4d3df521-a93a-4a73-bf38-d8a10a0f457d

📥 Commits

Reviewing files that changed from the base of the PR and between c9e1f2b and 7c1d92c.

📒 Files selected for processing (4)
  • crates/wa-enums/src/lib.rs
  • crates/wa-notif/src/actions.rs
  • crates/wa-notif/src/tests.rs
  • scripts/lint-ir.py

Comment thread crates/wa-notif/src/tests.rs
Comment thread scripts/lint-ir.py
`.filter()` ran on the COMBINED result, so `or_else` fired only when the
consequent was not a call at all. `cond ? otherCall() : map(…)` therefore
discarded the wrong call and never looked at the alternate — and both fixtures
used `0` as the consequent, where `as_call` already returns `None`, so the tests
could not see the gap they were written for. Each candidate is filtered on its
own now, with a call-consequent case added.

`parenRev` asserted only its `wire_tag`, which would have passed with the
collection wrongly marked always-present. It asserts the absence too.

@jlucaso1 jlucaso1 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Os dois achados fora do diff estão aplicados em 322fd44.

actions.rs 1942-1987 — fallback pulado quando o consequente é outra chamada. Correto, e a parte que mais importa é a que você notou sobre as fixtures: minhas duas usavam 0 como consequente, onde as_call já devolve None, então o or_else disparava por acidente e mascarava exatamente a lacuna que o fix pretendia fechar. Passei a filtrar cada candidato antes do fallback e adicionei cond ? otherCall() : map(…); sem o fix, no child callRev.

tests.rs 987 — parenRev sem asserção de required. Aplicado. Asserção só do wire_tag passaria com a coleção marcada como sempre presente.

Segunda vez nesta rodada que meu próprio teste escondia o defeito. Agradeço a leitura das fixtures e não só do diff — foi o que pegou os dois.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 322fd44f2c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-notif/src/actions.rs Outdated
Comment thread scripts/lint-ir.py
Comment thread scripts/lint-ir.py
…semantics

Three from Codex on 322fd44.

The branch search looked one level down, so `a ? x : (b ? y : map(…))` found the
inner conditional rather than the map inside it. `find_mapped_call` now recurses
through both sides; each step descends into a strictly smaller node.

Two linter checks, both of which had to be built around what the data actually
holds rather than the obvious rule:

- The accessor must agree with the declared type — but as a SET per accessor, not
  one type: `attrInt` legitimately backs `integer`, `timestamp` and
  `timestamp_millis`, and a one-to-one map would have failed CI on 27 correct
  fields.
- A `byteLength` belongs to an accessor that has one — checked only where a
  `method` is present, because 20 request-side `constBytes` pins carry the length
  with no accessor at all.

That is the third and fourth time a guard here would have rejected valid data if
written to the obvious rule; measuring the committed IR first is what caught both.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

(Some(x), Some(y)) => {
if x.wire_name == y.wire_name && x.method == y.method && x.content == y.content {

P2 Badge Compare enum tables when merging conditional reads

When both branches read the same wire attribute with the same enum accessor but validate it against different tables, this equality check treats them as the same read and publishes only x.enum_arg. For example, cond ? child.attrEnum("type", TABLE_A) : child.attrEnum("type", TABLE_B) can accept different values by branch while the IR advertises only TABLE_A as the closed constraint. Include the resolved enum table in branch compatibility, refusing or safely merging the field when the tables disagree.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-enums/src/lib.rs
Comment thread crates/wa-enums/src/lib.rs Outdated
Comment thread crates/wa-notif/src/actions.rs Outdated
jlucaso1 added 3 commits July 28, 2026 22:50
…both branches

Three from Codex on 4e7d5c5.

- A mutating `extends` was detected at the statement and declarator level only,
  so one nested in a sequence, a logical operand or a condition test was missed.
  Detected in a call visitor now, which the generic walk takes to every position.

- For a LEXICAL `for-in`/`for-of` head the bound name is already in its temporal
  dead zone while the right-hand side runs, so a read there throws rather than
  reaching an outer enum. Last round moved the RHS out of the scope for both
  kinds; that is right for `var` and wrong for `let`/`const`, and the two are now
  distinguished.

- `cond ? map("p", …) : map("q", …)` returned whichever branch was reached first,
  publishing one wire tag for a shape that produces two. Equal tags still resolve;
  differing ones refuse. The conditional is reconciled BEFORE the `strip_guard`
  shortcut, which follows the consequent and would otherwise answer with it.
… them

`privacyParser` reads `<privacy><category name="last" value=…/>…</privacy>`, where
the legal `value` enum depends on `name` and each category produces a differently
named output (`lastSeen`, `callAdd`, `readReceipts`, …). The IR publishes ten
sibling `value` fields at the response root: the nesting, the discriminator and
the output names are all gone, so a consumer sees one key with ten contradictory
constraints.

This predates the PR — `main` has the identical shape and `generated/iq` is not
among the files changed here. What is mine is the handling: the duplicate-key
check added in this PR found these 15, and I switched it off after reading them
as "alternatives the extractor models on purpose". Reading the source showed that
was wrong, and a linter whose stated purpose is "diagnostics over silence" should
not have certified them in the meantime.

Held to a baseline, like `dropsByReason` and the unresolved enums: the loss is
visible, cannot grow unnoticed, and CI does not fail on data this PR cannot fix.
Repairing the shape is an extractor change and belongs in its own PR.
Counting ARRAYS with a repeat meant an eleventh `value` in an array already
holding ten changed nothing — the same count-vs-identity blind spot the
unresolved-enum baseline had twice before. Counted per extra entry now, and the
baseline is the measured 42 rather than the 24 I first guessed. Both a new
repeat and an extra one inside an existing array trip it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

manifest.wasm = self.write_set(wasm, &wasm_dir)?;

P2 Badge Preserve cached wasm files until the manifest commits

When store_wasm updates an existing cache, write_set rewrites every reused payload at the same URL-derived path rather than alongside the old files. If an interrupt, full disk, or later write failure occurs after one of those files is truncated but before the new manifest is committed, the old manifest remains while its previously valid payload now fails size or checksum validation; because endpoint results are subsets, that payload may not be recoverable on the next run. Write reused paths atomically through temporary files, or avoid rewriting already verified entries, before committing the manifest.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/wa-enums/src/lib.rs
Comment thread crates/wa-notif/src/actions.rs Outdated
Comment thread scripts/lint-ir.py Outdated
Comment thread scripts/lint-ir.py
… shapes

Four from Codex on 7c837fa.

The flattening baseline I added two commits ago was an AGGREGATE, so one known
loss disappearing while a new one appeared elsewhere kept the total at 42 and
passed. That is the third time this exact blind spot has been found here and the
second in a baseline I wrote — after citing the lesson in the commit that
introduced it. Keyed by semantic path and repeated names now; a substitution
reports both halves.

- A deferred alias that disagrees with an inline write to the same export
  (`i.OUT = {A:"a"}; i.OUT = e`) kept the first body through `or_insert`, while
  the runtime exports the second. Same refusal the immediate path already makes.
- Two branches mapping the SAME tag with different callbacks published one of
  them, asserting `jid` for a branch that reads `lid`. Equal tag is not equal
  shape; the arguments must match too.
- A `*Source` is provenance for an inferred value: alone it says where a
  constraint came from without saying what to enforce. Both pairs are validated
  together, and an empty source string is rejected.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e332de1a2c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/lint-ir.py
Comment on lines +1155 to +1156
vf = a.get("valueField")
if isinstance(vf, str) and isinstance(vpt, str):

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 Require both AppState payload identifiers

Fresh evidence after the payload pair cross-check is that it runs only when both values are strings: setting an existing action's valueField to null while retaining its valid valueProtoType still leaves lint-ir.py successful, and the inverse is likewise accepted. The generated metadata then describes a protobuf payload without the SyncActionValue field where it must be placed, so require valueField and valueProtoType to be either both present or both absent before validating their correspondence.

Useful? React with 👍 / 👎.

Comment on lines 965 to 967
for (key, value) in wa_oxc::obj_props(o) {
if let Some(data) = enum_object(value).and_then(parse_enum) {
self.exports.entry(key.to_string()).or_insert(data);

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 Detect conflicting definitions inside export bags

Fresh evidence beyond the direct-property conflict fix is that inline members of X.exports = { ... } still use unconditional or_insert. For example, i.OUT = {A:"a"}; i.exports = {OUT:{B:"b"}} (or two OUT keys within export bags) keeps {A} here even though JavaScript's later write exports {B}; mark this as conflicting using the same comparison as the single-property path rather than publishing the stale closed enum.

Useful? React with 👍 / 👎.

Comment on lines +2149 to +2152
let same_text =
|x: &oxc_ast::ast::CallExpression, y: &oxc_ast::ast::CallExpression| {
x.span == y.span
|| format!("{:?}", x.arguments) == format!("{:?}", y.arguments)

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 Ignore source spans when matching mapped calls

Fresh evidence after reconciling callbacks is that the debug representation of Oxc arguments includes each node's source span, so two textually identical calls in different ternary branches never compare equal here (and their call spans cannot be equal either). Thus cond ? node.mapChildrenWithTag("p", parseP) : node.mapChildrenWithTag("p", parseP) is refused and its child metadata disappears despite both branches having the same shape; compare AST content without locations, such as through Oxc's content equality, instead.

Useful? React with 👍 / 👎.

Comment thread scripts/lint-ir.py
Comment on lines +443 to +447
if (
t == "enum"
and not f.get("enumRef")
and not f.get("enumKeys")
and not (f.get("protoEnum") and f["protoEnum"] in proto_enums)

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 Reject enum metadata on non-enum fields

When a normal string field such as method: "attrString", type: "string" is given a valid enumRef, every current check succeeds: the accessor agrees with the string type and the reference itself has variants. I applied that mutation to notif/index.json and the linter still certified all 12 documents, while notif_export publishes field_type: String together with a nonempty enum_values table, giving consumers contradictory answers about whether the value is constrained. Whenever a node has a type, reject enumRef (and the equivalent enumKeys/protoEnum metadata) unless that type is enum, while preserving request attributes that legitimately carry an enum reference through their kind vocabulary.

Useful? React with 👍 / 👎.

Comment thread scripts/lint-ir.py
Comment on lines +1365 to +1366
check_tokens(data, domain, errors)
check_notif_identifiers(data, domain, errors)

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 Validate WebAssembly hint values

When a resource's wasmHint contains an unknown string such as definitelyWrong, the schema accepts it and this linter still certifies all 12 documents. The IR contract defines exactly wasmBinaryLiteral, moduleName, and wasmModuleCacheDep, while the manifest counts any nonempty hint list toward wasmWasmHandles; consequently a typo is treated as recovered WebAssembly evidence even though consumers matching the documented values cannot interpret it. Add a WASM-domain check that rejects every hint outside the published vocabulary.

Useful? React with 👍 / 👎.

@jlucaso1
jlucaso1 merged commit c4fac5b into main Jul 29, 2026
4 checks passed
@jlucaso1
jlucaso1 deleted the ir-followups branch July 29, 2026 05:44
@jlucaso1
jlucaso1 restored the ir-followups branch July 29, 2026 05:46
@jlucaso1
jlucaso1 deleted the ir-followups branch July 29, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant