From 7be192495d713394930b2d2ff07be1253c9b1c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 21:40:37 +0000 Subject: [PATCH 01/52] test: add source-backed IQ and MEX conformance baseline --- .../wa-mex/tests/independent_conformance.rs | 83 ++++++ .../wa-scan/tests/independent_conformance.rs | 249 ++++++++++++++++++ crates/wa-transform/examples/module_spans.rs | 19 ++ .../WASmaxGroupsAcceptGroupAddRPC.js | 1 + .../WASmaxGroupsSetSubjectRPC.js | 1 + ...GroupAddResponseGroupJoinRequestSuccess.js | 1 + ...axInGroupsAcceptGroupAddResponseSuccess.js | 1 + .../WASmaxInGroupsIQResultResponseMixin.js | 1 + ...WASmaxInGroupsSetSubjectResponseSuccess.js | 1 + .../WASmaxOutGroupsAcceptGroupAddRequest.js | 1 + .../WASmaxOutGroupsBaseIQSetRequestMixin.js | 1 + .../WASmaxOutGroupsBaseSetGroupMixin.js | 1 + ...axOutGroupsSetSubjectChangeSubjectMixin.js | 1 + .../WASmaxOutGroupsSetSubjectRequest.js | 1 + .../WAWebMexFetchNewsletterJob.js | 1 + ...WAWebMexFetchNewsletterJobQuery.graphql.js | 1 + .../2.3000.1045368834/provenance.json | 99 +++++++ .../WASmaxGroupsAcceptGroupAddRPC.js | 1 + .../WASmaxGroupsSetSubjectRPC.js | 1 + ...GroupAddResponseGroupJoinRequestSuccess.js | 1 + ...axInGroupsAcceptGroupAddResponseSuccess.js | 1 + .../WASmaxInGroupsIQResultResponseMixin.js | 1 + ...WASmaxInGroupsSetSubjectResponseSuccess.js | 1 + .../WASmaxOutGroupsAcceptGroupAddRequest.js | 1 + .../WASmaxOutGroupsBaseIQSetRequestMixin.js | 1 + .../WASmaxOutGroupsBaseSetGroupMixin.js | 1 + ...axOutGroupsSetSubjectChangeSubjectMixin.js | 1 + .../WASmaxOutGroupsSetSubjectRequest.js | 1 + .../WAWebMexFetchNewsletterJob.js | 1 + ...WAWebMexFetchNewsletterJobQuery.graphql.js | 1 + .../2.3000.1047483476/provenance.json | 99 +++++++ tests/conformance/README.md | 179 +++++++++++++ tests/conformance/capture.py | 75 ++++++ tests/conformance/test_capture.py | 54 ++++ 34 files changed, 883 insertions(+) create mode 100644 crates/wa-mex/tests/independent_conformance.rs create mode 100644 crates/wa-scan/tests/independent_conformance.rs create mode 100644 crates/wa-transform/examples/module_spans.rs create mode 100644 tests/conformance/2.3000.1045368834/WASmaxGroupsAcceptGroupAddRPC.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxGroupsSetSubjectRPC.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseSuccess.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxInGroupsIQResultResponseMixin.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseSuccess.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxOutGroupsAcceptGroupAddRequest.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxOutGroupsBaseIQSetRequestMixin.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxOutGroupsBaseSetGroupMixin.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxOutGroupsSetSubjectChangeSubjectMixin.js create mode 100644 tests/conformance/2.3000.1045368834/WASmaxOutGroupsSetSubjectRequest.js create mode 100644 tests/conformance/2.3000.1045368834/WAWebMexFetchNewsletterJob.js create mode 100644 tests/conformance/2.3000.1045368834/WAWebMexFetchNewsletterJobQuery.graphql.js create mode 100644 tests/conformance/2.3000.1045368834/provenance.json create mode 100644 tests/conformance/2.3000.1047483476/WASmaxGroupsAcceptGroupAddRPC.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxGroupsSetSubjectRPC.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseSuccess.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxInGroupsIQResultResponseMixin.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseSuccess.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxOutGroupsAcceptGroupAddRequest.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxOutGroupsBaseIQSetRequestMixin.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxOutGroupsBaseSetGroupMixin.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxOutGroupsSetSubjectChangeSubjectMixin.js create mode 100644 tests/conformance/2.3000.1047483476/WASmaxOutGroupsSetSubjectRequest.js create mode 100644 tests/conformance/2.3000.1047483476/WAWebMexFetchNewsletterJob.js create mode 100644 tests/conformance/2.3000.1047483476/WAWebMexFetchNewsletterJobQuery.graphql.js create mode 100644 tests/conformance/2.3000.1047483476/provenance.json create mode 100644 tests/conformance/README.md create mode 100644 tests/conformance/capture.py create mode 100644 tests/conformance/test_capture.py diff --git a/crates/wa-mex/tests/independent_conformance.rs b/crates/wa-mex/tests/independent_conformance.rs new file mode 100644 index 00000000..299bbfec --- /dev/null +++ b/crates/wa-mex/tests/independent_conformance.rs @@ -0,0 +1,83 @@ +//! Presence is a serialization property, separate from inferred type/nullability. +use std::path::Path; +use wa_ir::{TypeNode, VariablePresence}; + +fn source(version: &str, module: &str) -> String { + std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/conformance") + .join(version) + .join(format!("{module}.js")), + ) + .unwrap() +} + +#[test] +fn verified_call_sites_keep_presence_separate_from_boolean_type() { + for version in ["2.3000.1045368834", "2.3000.1047483476"] { + let source = source(version, "WAWebMexFetchNewsletterJobQuery.graphql") + + &source(version, "WAWebMexFetchNewsletterJob"); + let ir = wa_mex::extract_mex(&source, version); + let op = &ir.operations["FetchNewsletter"]; + for (key, expected) in [ + ("fetch_creation_time", VariablePresence::Always), + ("fetch_full_image", VariablePresence::Always), + ("fetch_viewer_metadata", VariablePresence::Conditional), + ("fetch_pinned_messages", VariablePresence::Undetermined), + ] { + assert_eq!( + op.variables_presence[key].presence, expected, + "{version}: {key}" + ); + // All four are used as Relay Condition keys. That does not establish + // whether a call-site value can be null or undefined. + assert_eq!(op.variables_shape[key], TypeNode::Leaf("boolean".into())); + } + assert_eq!( + op.variables_presence["input"].presence, + VariablePresence::Always + ); + assert_eq!( + op.variables_presence["input"].fields["key"].presence, + VariablePresence::Conditional + ); + assert_eq!( + op.variables_presence["input"].fields["type"].presence, + VariablePresence::Always + ); + } +} + +#[test] +fn null_undefined_and_unresolved_calls_have_distinct_presence_verdicts() { + // Hand-authored perturbations of a real operation, not extra claims about + // which arguments the server accepts. Expectations follow JSON object-key + // serialization: null survives, undefined disappears, calls are unresolved. + let query = source( + "2.3000.1047483476", + "WAWebMexFetchNewsletterJobQuery.graphql", + ); + for (expression, expected) in [ + ("null", VariablePresence::Always), + ("false", VariablePresence::Always), + ("void 0", VariablePresence::Conditional), + ("t.flag", VariablePresence::Conditional), + ("t.flag === true", VariablePresence::Always), + ("o(\"External\").flag()", VariablePresence::Undetermined), + ] { + let caller = format!( + r#"__d("ConformanceCaller",["WAWebMexClient","WAWebMexFetchNewsletterJobQuery.graphql","External"],(function(t,n,r,o,a,i,l){{function f(t){{return o("WAWebMexClient").fetchQuery(n("WAWebMexFetchNewsletterJobQuery.graphql"),{{fetch_viewer_metadata:{expression}}})}}l.run=f}}),98);"# + ); + let ir = wa_mex::extract_mex(&(query.clone() + &caller), "fixture"); + let op = &ir.operations["FetchNewsletter"]; + assert_eq!( + op.variables_presence["fetch_viewer_metadata"].presence, expected, + "{expression}" + ); + assert_eq!( + op.variables_shape["fetch_viewer_metadata"], + TypeNode::Leaf("boolean".into()), + "presence does not establish nullability: {expression}" + ); + } +} diff --git a/crates/wa-scan/tests/independent_conformance.rs b/crates/wa-scan/tests/independent_conformance.rs new file mode 100644 index 00000000..0239c32a --- /dev/null +++ b/crates/wa-scan/tests/independent_conformance.rs @@ -0,0 +1,249 @@ +//! Static conformance for two reviewed paths, not execution of the Web client. +//! Expected wire cases are hand-reviewed against the captured module sources. +use serde_json::{Value, json}; +use std::path::Path; + +const VERSIONS: [&str; 2] = ["2.3000.1045368834", "2.3000.1047483476"]; + +fn captured(version: &str) -> Value { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/conformance") + .join(version); + let mut files: Vec<_> = std::fs::read_dir(root) + .unwrap() + .map(|e| e.unwrap().path()) + .filter(|p| { + p.file_name() + .unwrap() + .to_str() + .unwrap() + .starts_with("WASmax") + && p.extension().is_some_and(|e| e == "js") + }) + .collect(); + files.sort(); + let source = files + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"); + serde_json::to_value(wa_scan::extract_iq(&source, version)).unwrap() +} + +fn operation<'a>(ir: &'a Value, name: &str) -> &'a Value { + ir["stanzas"] + .as_array() + .unwrap() + .iter() + .find(|s| s["moduleName"] == format!("WASmaxOutGroups{name}Request")) + .unwrap_or_else(|| panic!("{name} missing")) +} + +fn request_contract(ir: &Value) { + for name in ["SetSubject", "AcceptGroupAdd"] { + let request = &operation(ir, name)["request"]; + assert_eq!(request["namespace"], "w:g2"); + assert_eq!(request["iqType"], "set"); + assert_eq!(request["target"], "group_jid"); + assert_eq!(request["targetArgPath"], json!([{"key":"iqTo"}])); + } + let subject = &operation(ir, "SetSubject")["request"]["children"][0]; + assert_eq!(subject["tag"], "subject"); + assert_eq!( + subject["content"]["argPath"], + json!([{"key":"subjectElementValue"}]) + ); + let accept = &operation(ir, "AcceptGroupAdd")["request"]["children"][0]; + assert_eq!(accept["tag"], "accept"); + for (wire, kind, arg) in [ + ("code", "string", "acceptCode"), + ("expiration", "integer", "acceptExpiration"), + ("admin", "user_jid", "acceptAdmin"), + ] { + let attr = accept["attrs"] + .as_array() + .unwrap() + .iter() + .find(|a| a["name"] == wire) + .unwrap(); + assert_eq!(attr["kind"], kind); + assert_eq!(attr["required"], true); + assert_eq!(attr["argPath"], json!([{"key":arg}])); + } +} + +// Deliberately restricted IR interpreter for success assertions. No field coercion, +// server-error parsing, nested child flattening, wire codec, or transport is implied. +// Unknown assertion kinds fail the test instead of silently accepting the input. +fn success<'a>(op: &'a Value, response: &Value, request: &Value) -> Option<&'a str> { + op["response"]["variants"] + .as_array() + .unwrap() + .iter() + .filter(|v| v["kind"] == "success") + .find(|v| { + v["assertions"].as_array().unwrap().iter().all(|a| { + let name = a["name"].as_str().unwrap(); + match a["kind"].as_str().unwrap() { + "tag" => response["tag"] == name, + "attr" => { + !response["attrs"][name].is_null() && response["attrs"][name] == a["value"] + } + "reference" => { + let path = a["referencePath"].as_array().unwrap(); + assert_eq!(path.len(), 1, "unsupported reference path"); + let expected = &request[path[0].as_str().unwrap()]; + !expected.is_null() + && !response["attrs"][name].is_null() + && response["attrs"][name] == *expected + } + "child" => response["children"] + .as_array() + .unwrap() + .iter() + .any(|c| c["tag"] == name), + other => panic!("unsupported assertion {other}"), + } + }) + }) + .map(|v| v["tag"].as_str().unwrap()) +} + +fn response_cases(ir: &Value) { + let request = json!({"id":"fixture-7","to":"120363000000001@g.us"}); + let bare = json!({"tag":"iq","attrs":{"id":"fixture-7","from":"120363000000001@g.us","type":"result"},"children":[]}); + let mut child = bare.clone(); + child["children"] = json!([{"tag":"membership_approval_request"}]); + let accept = operation(ir, "AcceptGroupAdd"); + let subject = operation(ir, "SetSubject"); + // This interpreter only covers the reviewed empty-payload successes. A new + // field needs an explicit extension, not accidental acceptance by this model. + for op in [accept, subject] { + for variant in op["response"]["variants"] + .as_array() + .unwrap() + .iter() + .filter(|v| v["kind"] == "success") + { + assert_eq!( + variant["fields"], + json!([{ + "method":"attrString", "name":"type", "wireName":"type", + "type":"string", "parserRequired":true, "literalValue":"result" + }]) + ); + } + } + assert_eq!( + success(accept, &bare, &request), + Some("AcceptGroupAddResponseSuccess") + ); + assert_eq!( + success(accept, &child, &request), + Some("AcceptGroupAddResponseGroupJoinRequestSuccess") + ); + assert_eq!( + success(subject, &bare, &request), + Some("SetSubjectResponseSuccess") + ); + // The reviewed bare parser does not reject extra children. Strict generation + // admission must not become blanket rejection of runtime extensions. + let mut extension = bare.clone(); + extension["children"] = json!([{"tag":"future_extension"}]); + assert_eq!( + success(accept, &extension, &request), + Some("AcceptGroupAddResponseSuccess") + ); + for op in [accept, subject] { + for valid in [&bare, &child] { + for (key, invalid) in [ + ("id", json!("other")), + ("id", Value::Null), + ("from", json!("g.us")), + ("from", Value::Null), + ("type", json!("set")), + ("type", Value::Null), + ] { + let mut response = valid.clone(); + response["attrs"][key] = invalid; + assert_eq!(success(op, &response, &request), None, "invalid {key}"); + } + for key in ["id", "from", "type"] { + let mut missing = valid.clone(); + missing["attrs"].as_object_mut().unwrap().remove(key); + assert_eq!( + success(op, &missing, &request), + None, + "absent response {key}" + ); + } + let mut response = valid.clone(); + response["tag"] = json!("message"); + assert_eq!(success(op, &response, &request), None); + for key in ["id", "to"] { + let mut missing = request.clone(); + missing.as_object_mut().unwrap().remove(key); + assert_eq!(success(op, valid, &missing), None, "missing request {key}"); + } + } + } + let tags: Vec<_> = accept["response"]["variants"] + .as_array() + .unwrap() + .iter() + .map(|v| v["tag"].as_str().unwrap()) + .collect(); + assert_eq!( + tags, + [ + "AcceptGroupAddResponseGroupJoinRequestSuccess", + "AcceptGroupAddResponseSuccess", + "AcceptGroupAddResponseClientError", + "AcceptGroupAddResponseServerError" + ] + ); +} + +#[test] +fn source_derived_contracts_hold_across_two_verified_snapshots() { + for version in VERSIONS { + let ir = captured(version); + request_contract(&ir); + response_cases(&ir); + } +} + +#[test] +fn committed_ir_satisfies_the_independent_wire_cases() { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../generated/iq/index.json"); + let ir = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + request_contract(&ir); + response_cases(&ir); +} + +#[test] +fn the_oracle_detects_lost_child_gate_and_reordered_outcomes() { + let ir = captured(VERSIONS[1]); + let request = json!({"id":"x","to":"g"}); + let bare = json!({"tag":"iq","attrs":{"id":"x","from":"g","type":"result"},"children":[]}); + let mut op = operation(&ir, "AcceptGroupAdd").clone(); + op["response"]["variants"][0]["assertions"] + .as_array_mut() + .unwrap() + .retain(|a| a["kind"] != "child"); + assert_ne!( + success(&op, &bare, &request), + Some("AcceptGroupAddResponseSuccess") + ); + let mut op = operation(&ir, "AcceptGroupAdd").clone(); + op["response"]["variants"] + .as_array_mut() + .unwrap() + .swap(0, 1); + let mut child = bare; + child["children"] = json!([{"tag":"membership_approval_request"}]); + assert_ne!( + success(&op, &child, &request), + Some("AcceptGroupAddResponseGroupJoinRequestSuccess") + ); +} diff --git a/crates/wa-transform/examples/module_spans.rs b/crates/wa-transform/examples/module_spans.rs new file mode 100644 index 00000000..a4676254 --- /dev/null +++ b/crates/wa-transform/examples/module_spans.rs @@ -0,0 +1,19 @@ +//! Static module spans for reviewed conformance captures; never executes JS. +//! Usage: module_spans BUNDLE MODULE [MODULE ...] +use anyhow::{Context, Result}; + +fn main() -> Result<()> { + let mut args = std::env::args().skip(1); + let path = args + .next() + .context("usage: module_spans BUNDLE MODULE ...")?; + let names: Vec<_> = args.collect(); + anyhow::ensure!(!names.is_empty(), "at least one module is required"); + let source = std::fs::read_to_string(path)?; + for module in wa_transform::extract_module_definitions(&source) { + if names.contains(&module.name) { + println!("{}\t{}\t{}", module.name, module.start, module.end); + } + } + Ok(()) +} diff --git a/tests/conformance/2.3000.1045368834/WASmaxGroupsAcceptGroupAddRPC.js b/tests/conformance/2.3000.1045368834/WASmaxGroupsAcceptGroupAddRPC.js new file mode 100644 index 00000000..8f887bd1 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxGroupsAcceptGroupAddRPC.js @@ -0,0 +1 @@ +__d("WASmaxGroupsAcceptGroupAddRPC",["WAComms","WASmaxInGroupsAcceptGroupAddResponseClientError","WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess","WASmaxInGroupsAcceptGroupAddResponseServerError","WASmaxInGroupsAcceptGroupAddResponseSuccess","WASmaxOutGroupsAcceptGroupAddRequest","WASmaxParsingFailure","WASmaxRpcUtils","asyncToGeneratorRuntime"],(function(t,n,r,o,a,i,l){function e(e,t){return s.apply(this,arguments)}function s(){return s=n("asyncToGeneratorRuntime").asyncToGenerator(function*(e,t){var n=o("WASmaxOutGroupsAcceptGroupAddRequest").makeAcceptGroupAddRequest(e),r=yield o("WAComms").sendSmaxStanza(n,t),a=o("WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess").parseAcceptGroupAddResponseGroupJoinRequestSuccess(r,n);if(a.success)return{name:"AcceptGroupAddResponseGroupJoinRequestSuccess",value:a.value};var i=o("WASmaxInGroupsAcceptGroupAddResponseSuccess").parseAcceptGroupAddResponseSuccess(r,n);if(i.success)return{name:"AcceptGroupAddResponseSuccess",value:i.value};var l=o("WASmaxInGroupsAcceptGroupAddResponseClientError").parseAcceptGroupAddResponseClientError(r,n);if(l.success)return{name:"AcceptGroupAddResponseClientError",value:l.value};var s=o("WASmaxInGroupsAcceptGroupAddResponseServerError").parseAcceptGroupAddResponseServerError(r,n);if(s.success)return{name:"AcceptGroupAddResponseServerError",value:s.value};throw new(o("WASmaxParsingFailure")).SmaxParsingFailure(o("WASmaxRpcUtils").errorMessageRpcParsing("AcceptGroupAdd",{GroupJoinRequestSuccess:a,Success:i,ClientError:l,ServerError:s}))}),s.apply(this,arguments)}l.sendAcceptGroupAddRPC=e}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxGroupsSetSubjectRPC.js b/tests/conformance/2.3000.1045368834/WASmaxGroupsSetSubjectRPC.js new file mode 100644 index 00000000..9a750b88 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxGroupsSetSubjectRPC.js @@ -0,0 +1 @@ +__d("WASmaxGroupsSetSubjectRPC",["WAComms","WASmaxInGroupsSetSubjectResponseClientError","WASmaxInGroupsSetSubjectResponseServerError","WASmaxInGroupsSetSubjectResponseSuccess","WASmaxOutGroupsSetSubjectRequest","WASmaxParsingFailure","WASmaxRpcUtils","asyncToGeneratorRuntime"],(function(t,n,r,o,a,i,l){function e(e,t){return s.apply(this,arguments)}function s(){return s=n("asyncToGeneratorRuntime").asyncToGenerator(function*(e,t){var n=o("WASmaxOutGroupsSetSubjectRequest").makeSetSubjectRequest(e),r=yield o("WAComms").sendSmaxStanza(n,t),a=o("WASmaxInGroupsSetSubjectResponseSuccess").parseSetSubjectResponseSuccess(r,n);if(a.success)return{name:"SetSubjectResponseSuccess",value:a.value};var i=o("WASmaxInGroupsSetSubjectResponseClientError").parseSetSubjectResponseClientError(r,n);if(i.success)return{name:"SetSubjectResponseClientError",value:i.value};var l=o("WASmaxInGroupsSetSubjectResponseServerError").parseSetSubjectResponseServerError(r,n);if(l.success)return{name:"SetSubjectResponseServerError",value:l.value};throw new(o("WASmaxParsingFailure")).SmaxParsingFailure(o("WASmaxRpcUtils").errorMessageRpcParsing("SetSubject",{Success:a,ClientError:i,ServerError:l}))}),s.apply(this,arguments)}l.sendSetSubjectRPC=e}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess.js b/tests/conformance/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess.js new file mode 100644 index 00000000..088b343a --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess",["WASmaxInGroupsIQResultResponseMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseUtils").flattenedChildWithTag(e,"membership_approval_request");if(!r.success)return r;var a=o("WASmaxInGroupsIQResultResponseMixin").parseIQResultResponseMixin(e,t);return a.success,a}l.parseAcceptGroupAddResponseGroupJoinRequestSuccess=e}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseSuccess.js b/tests/conformance/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseSuccess.js new file mode 100644 index 00000000..c7a45b2f --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseSuccess.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddResponseSuccess",["WASmaxInGroupsIQResultResponseMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxInGroupsIQResultResponseMixin").parseIQResultResponseMixin(e,t);return r.success,r}l.parseAcceptGroupAddResponseSuccess=e}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxInGroupsIQResultResponseMixin.js b/tests/conformance/2.3000.1045368834/WASmaxInGroupsIQResultResponseMixin.js new file mode 100644 index 00000000..79feb1e5 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxInGroupsIQResultResponseMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQResultResponseMixin",["WAResultOrError","WASmaxParseReference","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseReference").attrStringFromReference(t,["id"]);if(!r.success)return r;var a=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"id",r.value);if(!a.success)return a;var i=o("WASmaxParseReference").attrStringFromReference(t,["to"]);if(!i.success)return i;var l=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"from",i.value);if(!l.success)return l;var s=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"type","result");return s.success?o("WAResultOrError").makeResult({type:s.value}):s}l.parseIQResultResponseMixin=e}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseSuccess.js b/tests/conformance/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseSuccess.js new file mode 100644 index 00000000..962025f1 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseSuccess.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsSetSubjectResponseSuccess",["WASmaxInGroupsIQResultResponseMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxInGroupsIQResultResponseMixin").parseIQResultResponseMixin(e,t);return r.success,r}l.parseSetSubjectResponseSuccess=e}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxOutGroupsAcceptGroupAddRequest.js b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsAcceptGroupAddRequest.js new file mode 100644 index 00000000..642073a2 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsAcceptGroupAddRequest.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsAcceptGroupAddRequest",["WASmaxJsx","WASmaxOutGroupsBaseSetGroupMixin","WAWap"],(function(t,n,r,o,a,i,l){function e(e){var t=e.acceptCode,n=e.acceptExpiration,r=e.acceptAdmin,a=o("WASmaxOutGroupsBaseSetGroupMixin").mergeBaseSetGroupMixin(o("WASmaxJsx").smax("iq",null,o("WASmaxJsx").smax("accept",{code:o("WAWap").CUSTOM_STRING(t),expiration:o("WAWap").INT(n),admin:o("WAWap").USER_JID(r)})),e);return a}l.makeAcceptGroupAddRequest=e}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxOutGroupsBaseIQSetRequestMixin.js b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsBaseIQSetRequestMixin.js new file mode 100644 index 00000000..d701770b --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsBaseIQSetRequestMixin.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsBaseIQSetRequestMixin",["WASmaxJsx","WASmaxMixins","WAWap"],(function(t,n,r,o,a,i,l){function e(){var e=o("WASmaxJsx").smax("iq",{id:o("WAWap").generateId(),type:"set"});return e}function s(t){var n=e();return o("WASmaxMixins").mergeStanzas(t,n)}l.mergeBaseIQSetRequestMixin=s}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxOutGroupsBaseSetGroupMixin.js b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsBaseSetGroupMixin.js new file mode 100644 index 00000000..89a18947 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsBaseSetGroupMixin.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsBaseSetGroupMixin",["WASmaxJsx","WASmaxMixins","WASmaxOutGroupsBaseIQSetRequestMixin","WAWap"],(function(t,n,r,o,a,i,l){function e(e){var t=e.iqTo,n=o("WASmaxOutGroupsBaseIQSetRequestMixin").mergeBaseIQSetRequestMixin(o("WASmaxJsx").smax("iq",{to:o("WAWap").GROUP_JID(t),xmlns:"w:g2"}));return n}function s(t,n){var r=e(n);return o("WASmaxMixins").mergeStanzas(t,r)}l.mergeBaseSetGroupMixin=s}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxOutGroupsSetSubjectChangeSubjectMixin.js b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsSetSubjectChangeSubjectMixin.js new file mode 100644 index 00000000..42a02b7e --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsSetSubjectChangeSubjectMixin.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsSetSubjectChangeSubjectMixin",["WASmaxJsx","WASmaxMixins"],(function(t,n,r,o,a,i,l){function e(e){var t=e.subjectElementValue,n=o("WASmaxJsx").smax("iq",null,o("WASmaxJsx").smax("subject",null,t));return n}function s(t,n){var r=e(n);return o("WASmaxMixins").mergeStanzas(t,r)}l.mergeSetSubjectChangeSubjectMixin=s}),98); diff --git a/tests/conformance/2.3000.1045368834/WASmaxOutGroupsSetSubjectRequest.js b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsSetSubjectRequest.js new file mode 100644 index 00000000..31a05c34 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WASmaxOutGroupsSetSubjectRequest.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsSetSubjectRequest",["WASmaxJsx","WASmaxOutGroupsBaseSetGroupMixin","WASmaxOutGroupsSetSubjectChangeSubjectMixin"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxOutGroupsSetSubjectChangeSubjectMixin").mergeSetSubjectChangeSubjectMixin(o("WASmaxOutGroupsBaseSetGroupMixin").mergeBaseSetGroupMixin(o("WASmaxJsx").smax("iq",null),e),e);return t}l.makeSetSubjectRequest=e}),98); diff --git a/tests/conformance/2.3000.1045368834/WAWebMexFetchNewsletterJob.js b/tests/conformance/2.3000.1045368834/WAWebMexFetchNewsletterJob.js new file mode 100644 index 00000000..c2af2fdc --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WAWebMexFetchNewsletterJob.js @@ -0,0 +1 @@ +__d("WAWebMexFetchNewsletterJob",["WALogger","WAWebMexClient","WAWebMexFetchNewsletterJobQuery.graphql","WAWebNewsletterPinGatingUtils","WAWebWid","asyncToGeneratorRuntime"],(function(t,n,r,o,a,i,l){var e,s;function u(e,t,n){return c.apply(this,arguments)}function c(){return c=n("asyncToGeneratorRuntime").asyncToGenerator(function*(t,a,i){var l=e!==void 0?e:e=n("WAWebMexFetchNewsletterJobQuery.graphql"),u=r("WAWebWid").isNewsletter(t)?"JID":"INVITE",c=u!=="INVITE",d={input:{key:t,type:u,view_role:a},fetch_viewer_metadata:i.fetchViewerMetadata,fetch_full_image:c,fetch_creation_time:i.fetchCreationTime===!0,fetch_wamo_sub:i.fetchWamoSub===!0,fetch_status_metadata:i.fetchStatusMetadata===!0,fetch_pinned_messages:o("WAWebNewsletterPinGatingUtils").isChannelMessagePinReadEnabled()},m=yield o("WAWebMexClient").fetchQuery(l,d);return o("WALogger").LOG(s||(s=babelHelpers.taggedTemplateLiteralLoose(["[MEX][NEWSLETTER] fetched get newsletter job for ",""])),t).tags("GQL","MEX"),m}),c.apply(this,arguments)}l.mexGetNewsletter=u}),98); diff --git a/tests/conformance/2.3000.1045368834/WAWebMexFetchNewsletterJobQuery.graphql.js b/tests/conformance/2.3000.1045368834/WAWebMexFetchNewsletterJobQuery.graphql.js new file mode 100644 index 00000000..2bb3ddd3 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/WAWebMexFetchNewsletterJobQuery.graphql.js @@ -0,0 +1 @@ +__d("WAWebMexFetchNewsletterJobQuery.graphql",[],(function(t,n,r,o,a,i){"use strict";var e=(function(){var e={defaultValue:null,kind:"LocalArgument",name:"fetch_creation_time"},t={defaultValue:null,kind:"LocalArgument",name:"fetch_full_image"},n={defaultValue:null,kind:"LocalArgument",name:"fetch_pinned_messages"},r={defaultValue:null,kind:"LocalArgument",name:"fetch_status_metadata"},o={defaultValue:null,kind:"LocalArgument",name:"fetch_viewer_metadata"},a={defaultValue:null,kind:"LocalArgument",name:"fetch_wamo_sub"},i={defaultValue:null,kind:"LocalArgument",name:"input"},l={alias:null,args:null,kind:"ScalarField",name:"id",storageKey:null},s={alias:null,args:null,kind:"ScalarField",name:"type",storageKey:null},u=[l,{alias:null,args:null,kind:"ScalarField",name:"text",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"update_time",storageKey:null}],c=[l,s,{alias:null,args:null,kind:"ScalarField",name:"direct_path",storageKey:null}],d={alias:null,args:null,kind:"ScalarField",name:"value",storageKey:null},m=[{alias:null,args:[{kind:"Variable",name:"input",variableName:"input"}],concreteType:"XWA2Newsletter",kind:"LinkedField",name:"xwa2_newsletter",plural:!1,selections:[l,{alias:null,args:null,concreteType:"XWA2NewsletterState",kind:"LinkedField",name:"state",plural:!1,selections:[s],storageKey:null},{alias:null,args:null,concreteType:"XWA2NewsletterThreadMetadata",kind:"LinkedField",name:"thread_metadata",plural:!1,selections:[{condition:"fetch_creation_time",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,kind:"ScalarField",name:"creation_time",storageKey:null}]},{alias:null,args:null,concreteType:"XWA2NewsletterName",kind:"LinkedField",name:"name",plural:!1,selections:u,storageKey:null},{condition:"fetch_full_image",kind:"Condition",passingValue:!0,selections:[{alias:null,args:[{kind:"Literal",name:"type",value:"IMAGE"}],concreteType:"XWA2Picture",kind:"LinkedField",name:"picture",plural:!1,selections:c,storageKey:'picture(type:"IMAGE")'}]},{alias:"preview",args:[{kind:"Literal",name:"type",value:"PREVIEW"}],concreteType:"XWA2Picture",kind:"LinkedField",name:"picture",plural:!1,selections:c,storageKey:'picture(type:"PREVIEW")'},{alias:null,args:null,concreteType:"XWA2NewsletterDescription",kind:"LinkedField",name:"description",plural:!1,selections:u,storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"invite",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"handle",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"subscribers_count",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"verification",storageKey:null},{alias:null,args:null,concreteType:"XWA2NewsletterThreadSettings",kind:"LinkedField",name:"settings",plural:!1,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterReactionCodesSetting",kind:"LinkedField",name:"reaction_codes",plural:!1,selections:[d],storageKey:null}],storageKey:null},{condition:"fetch_wamo_sub",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterWamoSubMetadata",kind:"LinkedField",name:"wamo_sub",plural:!1,selections:[{alias:null,args:null,kind:"ScalarField",name:"plan_id",storageKey:null}],storageKey:null}]},{condition:"fetch_pinned_messages",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterPinnedMessage",kind:"LinkedField",name:"pinned_messages",plural:!0,selections:[{alias:null,args:null,kind:"ScalarField",name:"message_id",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"expiry_ts",storageKey:null}],storageKey:null}]}],storageKey:null},{condition:"fetch_viewer_metadata",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterViewerMetadata",kind:"LinkedField",name:"viewer_metadata",plural:!1,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterUserSetting",kind:"LinkedField",name:"settings",plural:!0,selections:[s,d],storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"role",storageKey:null},{condition:"fetch_wamo_sub",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,kind:"ScalarField",name:"wamo_sub_status",storageKey:null}]}],storageKey:null}]},{condition:"fetch_status_metadata",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterStatusMetadata",kind:"LinkedField",name:"status_metadata",plural:!1,selections:[{alias:null,args:null,kind:"ScalarField",name:"last_status_server_id",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"last_status_sent_time",storageKey:null}],storageKey:null}]}],storageKey:null}];return{fragment:{argumentDefinitions:[e,t,n,r,o,a,i],kind:"Fragment",metadata:null,name:"WAWebMexFetchNewsletterJobQuery",selections:m,type:"Query",abstractKey:null},kind:"Request",operation:{argumentDefinitions:[i,o,t,e,a,r,n],kind:"Operation",name:"WAWebMexFetchNewsletterJobQuery",selections:m},params:{id:"27456920720571478",metadata:{},name:"WAWebMexFetchNewsletterJobQuery",operationKind:"query",text:null}}})();a.exports=e}),null); diff --git a/tests/conformance/2.3000.1045368834/provenance.json b/tests/conformance/2.3000.1045368834/provenance.json new file mode 100644 index 00000000..47865063 --- /dev/null +++ b/tests/conformance/2.3000.1045368834/provenance.json @@ -0,0 +1,99 @@ +{ + "waVersion": "2.3000.1045368834", + "setHash": "99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00", + "bundleCount": 516, + "bundleBytes": 77347149, + "modules": [ + { + "module": "WASmaxGroupsAcceptGroupAddRPC", + "bundleSha256": "91d65febb587cea7283313c48528407fce6e949cdcb7a29cd2e0d9df1d21b1a8", + "start": 5922, + "end": 7533, + "sourceSha256": "2167e222e0875e85276bd519e5bd86839ba1e92808af5a1320b424d84ca3d7ae" + }, + { + "module": "WASmaxGroupsSetSubjectRPC", + "bundleSha256": "ed2f08bc12a1128a06ddafe25fc62e5d102593245a8b5e818fd8f2eedab3ffb9", + "start": 28590, + "end": 29826, + "sourceSha256": "ddca755916395cc8f7b5d9cc1586e61030e0aad4f77e65385f86a09a47a3938d" + }, + { + "module": "WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess", + "bundleSha256": "91d65febb587cea7283313c48528407fce6e949cdcb7a29cd2e0d9df1d21b1a8", + "start": 4229, + "end": 4728, + "sourceSha256": "3e137254ccda2e5b4b5d97fd512fe1ff74449cb360886195ecd46e098956718a" + }, + { + "module": "WASmaxInGroupsAcceptGroupAddResponseSuccess", + "bundleSha256": "91d65febb587cea7283313c48528407fce6e949cdcb7a29cd2e0d9df1d21b1a8", + "start": 5098, + "end": 5459, + "sourceSha256": "2881a4c961499317969ca0c9a4ddbbd41cc60f4c0ac6567467e4ffc9cce62354" + }, + { + "module": "WASmaxInGroupsIQResultResponseMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 107365, + "end": 108166, + "sourceSha256": "5bb1d7b81b3b9658b04c755236384c73e1e8d4f7bffecd2718d473738b3f387d" + }, + { + "module": "WASmaxInGroupsSetSubjectResponseSuccess", + "bundleSha256": "ed2f08bc12a1128a06ddafe25fc62e5d102593245a8b5e818fd8f2eedab3ffb9", + "start": 27492, + "end": 27845, + "sourceSha256": "0e40e8f7578b1ef63af1cd3aa9a67de5c161b02faaa7101f716bbb1dc68fa8e8" + }, + { + "module": "WASmaxOutGroupsAcceptGroupAddRequest", + "bundleSha256": "91d65febb587cea7283313c48528407fce6e949cdcb7a29cd2e0d9df1d21b1a8", + "start": 5461, + "end": 5920, + "sourceSha256": "5cf14463914ca9f49171d4e081e1addbfcb1c061a0c4b87e0a8325dad6240c82" + }, + { + "module": "WASmaxOutGroupsBaseIQSetRequestMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 127931, + "end": 128233, + "sourceSha256": "66d7fb959f7fd555178fb69e4a8d5223cd87b5e010aee7c0c14310bd7f5aaa90" + }, + { + "module": "WASmaxOutGroupsBaseSetGroupMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 128235, + "end": 128653, + "sourceSha256": "0a5176779f9969f702da985eef658a46d846d9bf355f709ce58f7490d0d33fc7" + }, + { + "module": "WASmaxOutGroupsSetSubjectChangeSubjectMixin", + "bundleSha256": "ed2f08bc12a1128a06ddafe25fc62e5d102593245a8b5e818fd8f2eedab3ffb9", + "start": 27847, + "end": 28186, + "sourceSha256": "3237a975959ca40026f296626e39cf6310eb57288f0cb9a79ce97edbc5714dec" + }, + { + "module": "WASmaxOutGroupsSetSubjectRequest", + "bundleSha256": "ed2f08bc12a1128a06ddafe25fc62e5d102593245a8b5e818fd8f2eedab3ffb9", + "start": 28188, + "end": 28588, + "sourceSha256": "b5f5dfc3427e0eadc42c98fd1b43e20e50451c8a03c70f6ef941353899de746c" + }, + { + "module": "WAWebMexFetchNewsletterJob", + "bundleSha256": "ffe69f61924eb23c1d65cb89a152e4ce35ee404bd52a7edcced9c037b236f490", + "start": 4364616, + "end": 4365653, + "sourceSha256": "67d888be41361e8443f0c13795cc1485e325923909a76a37308eeeebbf8e9d4a" + }, + { + "module": "WAWebMexFetchNewsletterJobQuery.graphql", + "bundleSha256": "ffe69f61924eb23c1d65cb89a152e4ce35ee404bd52a7edcced9c037b236f490", + "start": 4359525, + "end": 4364614, + "sourceSha256": "c45101869de13508fddd0c7d0f28289079d4835ea6f4919ec5d1564668288458" + } + ] +} diff --git a/tests/conformance/2.3000.1047483476/WASmaxGroupsAcceptGroupAddRPC.js b/tests/conformance/2.3000.1047483476/WASmaxGroupsAcceptGroupAddRPC.js new file mode 100644 index 00000000..8f887bd1 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxGroupsAcceptGroupAddRPC.js @@ -0,0 +1 @@ +__d("WASmaxGroupsAcceptGroupAddRPC",["WAComms","WASmaxInGroupsAcceptGroupAddResponseClientError","WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess","WASmaxInGroupsAcceptGroupAddResponseServerError","WASmaxInGroupsAcceptGroupAddResponseSuccess","WASmaxOutGroupsAcceptGroupAddRequest","WASmaxParsingFailure","WASmaxRpcUtils","asyncToGeneratorRuntime"],(function(t,n,r,o,a,i,l){function e(e,t){return s.apply(this,arguments)}function s(){return s=n("asyncToGeneratorRuntime").asyncToGenerator(function*(e,t){var n=o("WASmaxOutGroupsAcceptGroupAddRequest").makeAcceptGroupAddRequest(e),r=yield o("WAComms").sendSmaxStanza(n,t),a=o("WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess").parseAcceptGroupAddResponseGroupJoinRequestSuccess(r,n);if(a.success)return{name:"AcceptGroupAddResponseGroupJoinRequestSuccess",value:a.value};var i=o("WASmaxInGroupsAcceptGroupAddResponseSuccess").parseAcceptGroupAddResponseSuccess(r,n);if(i.success)return{name:"AcceptGroupAddResponseSuccess",value:i.value};var l=o("WASmaxInGroupsAcceptGroupAddResponseClientError").parseAcceptGroupAddResponseClientError(r,n);if(l.success)return{name:"AcceptGroupAddResponseClientError",value:l.value};var s=o("WASmaxInGroupsAcceptGroupAddResponseServerError").parseAcceptGroupAddResponseServerError(r,n);if(s.success)return{name:"AcceptGroupAddResponseServerError",value:s.value};throw new(o("WASmaxParsingFailure")).SmaxParsingFailure(o("WASmaxRpcUtils").errorMessageRpcParsing("AcceptGroupAdd",{GroupJoinRequestSuccess:a,Success:i,ClientError:l,ServerError:s}))}),s.apply(this,arguments)}l.sendAcceptGroupAddRPC=e}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxGroupsSetSubjectRPC.js b/tests/conformance/2.3000.1047483476/WASmaxGroupsSetSubjectRPC.js new file mode 100644 index 00000000..9a750b88 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxGroupsSetSubjectRPC.js @@ -0,0 +1 @@ +__d("WASmaxGroupsSetSubjectRPC",["WAComms","WASmaxInGroupsSetSubjectResponseClientError","WASmaxInGroupsSetSubjectResponseServerError","WASmaxInGroupsSetSubjectResponseSuccess","WASmaxOutGroupsSetSubjectRequest","WASmaxParsingFailure","WASmaxRpcUtils","asyncToGeneratorRuntime"],(function(t,n,r,o,a,i,l){function e(e,t){return s.apply(this,arguments)}function s(){return s=n("asyncToGeneratorRuntime").asyncToGenerator(function*(e,t){var n=o("WASmaxOutGroupsSetSubjectRequest").makeSetSubjectRequest(e),r=yield o("WAComms").sendSmaxStanza(n,t),a=o("WASmaxInGroupsSetSubjectResponseSuccess").parseSetSubjectResponseSuccess(r,n);if(a.success)return{name:"SetSubjectResponseSuccess",value:a.value};var i=o("WASmaxInGroupsSetSubjectResponseClientError").parseSetSubjectResponseClientError(r,n);if(i.success)return{name:"SetSubjectResponseClientError",value:i.value};var l=o("WASmaxInGroupsSetSubjectResponseServerError").parseSetSubjectResponseServerError(r,n);if(l.success)return{name:"SetSubjectResponseServerError",value:l.value};throw new(o("WASmaxParsingFailure")).SmaxParsingFailure(o("WASmaxRpcUtils").errorMessageRpcParsing("SetSubject",{Success:a,ClientError:i,ServerError:l}))}),s.apply(this,arguments)}l.sendSetSubjectRPC=e}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess.js b/tests/conformance/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess.js new file mode 100644 index 00000000..088b343a --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess",["WASmaxInGroupsIQResultResponseMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseUtils").flattenedChildWithTag(e,"membership_approval_request");if(!r.success)return r;var a=o("WASmaxInGroupsIQResultResponseMixin").parseIQResultResponseMixin(e,t);return a.success,a}l.parseAcceptGroupAddResponseGroupJoinRequestSuccess=e}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseSuccess.js b/tests/conformance/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseSuccess.js new file mode 100644 index 00000000..c7a45b2f --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseSuccess.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddResponseSuccess",["WASmaxInGroupsIQResultResponseMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxInGroupsIQResultResponseMixin").parseIQResultResponseMixin(e,t);return r.success,r}l.parseAcceptGroupAddResponseSuccess=e}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxInGroupsIQResultResponseMixin.js b/tests/conformance/2.3000.1047483476/WASmaxInGroupsIQResultResponseMixin.js new file mode 100644 index 00000000..79feb1e5 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxInGroupsIQResultResponseMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQResultResponseMixin",["WAResultOrError","WASmaxParseReference","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseReference").attrStringFromReference(t,["id"]);if(!r.success)return r;var a=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"id",r.value);if(!a.success)return a;var i=o("WASmaxParseReference").attrStringFromReference(t,["to"]);if(!i.success)return i;var l=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"from",i.value);if(!l.success)return l;var s=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"type","result");return s.success?o("WAResultOrError").makeResult({type:s.value}):s}l.parseIQResultResponseMixin=e}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseSuccess.js b/tests/conformance/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseSuccess.js new file mode 100644 index 00000000..962025f1 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseSuccess.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsSetSubjectResponseSuccess",["WASmaxInGroupsIQResultResponseMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxInGroupsIQResultResponseMixin").parseIQResultResponseMixin(e,t);return r.success,r}l.parseSetSubjectResponseSuccess=e}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxOutGroupsAcceptGroupAddRequest.js b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsAcceptGroupAddRequest.js new file mode 100644 index 00000000..642073a2 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsAcceptGroupAddRequest.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsAcceptGroupAddRequest",["WASmaxJsx","WASmaxOutGroupsBaseSetGroupMixin","WAWap"],(function(t,n,r,o,a,i,l){function e(e){var t=e.acceptCode,n=e.acceptExpiration,r=e.acceptAdmin,a=o("WASmaxOutGroupsBaseSetGroupMixin").mergeBaseSetGroupMixin(o("WASmaxJsx").smax("iq",null,o("WASmaxJsx").smax("accept",{code:o("WAWap").CUSTOM_STRING(t),expiration:o("WAWap").INT(n),admin:o("WAWap").USER_JID(r)})),e);return a}l.makeAcceptGroupAddRequest=e}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxOutGroupsBaseIQSetRequestMixin.js b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsBaseIQSetRequestMixin.js new file mode 100644 index 00000000..d701770b --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsBaseIQSetRequestMixin.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsBaseIQSetRequestMixin",["WASmaxJsx","WASmaxMixins","WAWap"],(function(t,n,r,o,a,i,l){function e(){var e=o("WASmaxJsx").smax("iq",{id:o("WAWap").generateId(),type:"set"});return e}function s(t){var n=e();return o("WASmaxMixins").mergeStanzas(t,n)}l.mergeBaseIQSetRequestMixin=s}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxOutGroupsBaseSetGroupMixin.js b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsBaseSetGroupMixin.js new file mode 100644 index 00000000..89a18947 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsBaseSetGroupMixin.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsBaseSetGroupMixin",["WASmaxJsx","WASmaxMixins","WASmaxOutGroupsBaseIQSetRequestMixin","WAWap"],(function(t,n,r,o,a,i,l){function e(e){var t=e.iqTo,n=o("WASmaxOutGroupsBaseIQSetRequestMixin").mergeBaseIQSetRequestMixin(o("WASmaxJsx").smax("iq",{to:o("WAWap").GROUP_JID(t),xmlns:"w:g2"}));return n}function s(t,n){var r=e(n);return o("WASmaxMixins").mergeStanzas(t,r)}l.mergeBaseSetGroupMixin=s}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxOutGroupsSetSubjectChangeSubjectMixin.js b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsSetSubjectChangeSubjectMixin.js new file mode 100644 index 00000000..42a02b7e --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsSetSubjectChangeSubjectMixin.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsSetSubjectChangeSubjectMixin",["WASmaxJsx","WASmaxMixins"],(function(t,n,r,o,a,i,l){function e(e){var t=e.subjectElementValue,n=o("WASmaxJsx").smax("iq",null,o("WASmaxJsx").smax("subject",null,t));return n}function s(t,n){var r=e(n);return o("WASmaxMixins").mergeStanzas(t,r)}l.mergeSetSubjectChangeSubjectMixin=s}),98); diff --git a/tests/conformance/2.3000.1047483476/WASmaxOutGroupsSetSubjectRequest.js b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsSetSubjectRequest.js new file mode 100644 index 00000000..31a05c34 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WASmaxOutGroupsSetSubjectRequest.js @@ -0,0 +1 @@ +__d("WASmaxOutGroupsSetSubjectRequest",["WASmaxJsx","WASmaxOutGroupsBaseSetGroupMixin","WASmaxOutGroupsSetSubjectChangeSubjectMixin"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxOutGroupsSetSubjectChangeSubjectMixin").mergeSetSubjectChangeSubjectMixin(o("WASmaxOutGroupsBaseSetGroupMixin").mergeBaseSetGroupMixin(o("WASmaxJsx").smax("iq",null),e),e);return t}l.makeSetSubjectRequest=e}),98); diff --git a/tests/conformance/2.3000.1047483476/WAWebMexFetchNewsletterJob.js b/tests/conformance/2.3000.1047483476/WAWebMexFetchNewsletterJob.js new file mode 100644 index 00000000..c2af2fdc --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WAWebMexFetchNewsletterJob.js @@ -0,0 +1 @@ +__d("WAWebMexFetchNewsletterJob",["WALogger","WAWebMexClient","WAWebMexFetchNewsletterJobQuery.graphql","WAWebNewsletterPinGatingUtils","WAWebWid","asyncToGeneratorRuntime"],(function(t,n,r,o,a,i,l){var e,s;function u(e,t,n){return c.apply(this,arguments)}function c(){return c=n("asyncToGeneratorRuntime").asyncToGenerator(function*(t,a,i){var l=e!==void 0?e:e=n("WAWebMexFetchNewsletterJobQuery.graphql"),u=r("WAWebWid").isNewsletter(t)?"JID":"INVITE",c=u!=="INVITE",d={input:{key:t,type:u,view_role:a},fetch_viewer_metadata:i.fetchViewerMetadata,fetch_full_image:c,fetch_creation_time:i.fetchCreationTime===!0,fetch_wamo_sub:i.fetchWamoSub===!0,fetch_status_metadata:i.fetchStatusMetadata===!0,fetch_pinned_messages:o("WAWebNewsletterPinGatingUtils").isChannelMessagePinReadEnabled()},m=yield o("WAWebMexClient").fetchQuery(l,d);return o("WALogger").LOG(s||(s=babelHelpers.taggedTemplateLiteralLoose(["[MEX][NEWSLETTER] fetched get newsletter job for ",""])),t).tags("GQL","MEX"),m}),c.apply(this,arguments)}l.mexGetNewsletter=u}),98); diff --git a/tests/conformance/2.3000.1047483476/WAWebMexFetchNewsletterJobQuery.graphql.js b/tests/conformance/2.3000.1047483476/WAWebMexFetchNewsletterJobQuery.graphql.js new file mode 100644 index 00000000..2bb3ddd3 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/WAWebMexFetchNewsletterJobQuery.graphql.js @@ -0,0 +1 @@ +__d("WAWebMexFetchNewsletterJobQuery.graphql",[],(function(t,n,r,o,a,i){"use strict";var e=(function(){var e={defaultValue:null,kind:"LocalArgument",name:"fetch_creation_time"},t={defaultValue:null,kind:"LocalArgument",name:"fetch_full_image"},n={defaultValue:null,kind:"LocalArgument",name:"fetch_pinned_messages"},r={defaultValue:null,kind:"LocalArgument",name:"fetch_status_metadata"},o={defaultValue:null,kind:"LocalArgument",name:"fetch_viewer_metadata"},a={defaultValue:null,kind:"LocalArgument",name:"fetch_wamo_sub"},i={defaultValue:null,kind:"LocalArgument",name:"input"},l={alias:null,args:null,kind:"ScalarField",name:"id",storageKey:null},s={alias:null,args:null,kind:"ScalarField",name:"type",storageKey:null},u=[l,{alias:null,args:null,kind:"ScalarField",name:"text",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"update_time",storageKey:null}],c=[l,s,{alias:null,args:null,kind:"ScalarField",name:"direct_path",storageKey:null}],d={alias:null,args:null,kind:"ScalarField",name:"value",storageKey:null},m=[{alias:null,args:[{kind:"Variable",name:"input",variableName:"input"}],concreteType:"XWA2Newsletter",kind:"LinkedField",name:"xwa2_newsletter",plural:!1,selections:[l,{alias:null,args:null,concreteType:"XWA2NewsletterState",kind:"LinkedField",name:"state",plural:!1,selections:[s],storageKey:null},{alias:null,args:null,concreteType:"XWA2NewsletterThreadMetadata",kind:"LinkedField",name:"thread_metadata",plural:!1,selections:[{condition:"fetch_creation_time",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,kind:"ScalarField",name:"creation_time",storageKey:null}]},{alias:null,args:null,concreteType:"XWA2NewsletterName",kind:"LinkedField",name:"name",plural:!1,selections:u,storageKey:null},{condition:"fetch_full_image",kind:"Condition",passingValue:!0,selections:[{alias:null,args:[{kind:"Literal",name:"type",value:"IMAGE"}],concreteType:"XWA2Picture",kind:"LinkedField",name:"picture",plural:!1,selections:c,storageKey:'picture(type:"IMAGE")'}]},{alias:"preview",args:[{kind:"Literal",name:"type",value:"PREVIEW"}],concreteType:"XWA2Picture",kind:"LinkedField",name:"picture",plural:!1,selections:c,storageKey:'picture(type:"PREVIEW")'},{alias:null,args:null,concreteType:"XWA2NewsletterDescription",kind:"LinkedField",name:"description",plural:!1,selections:u,storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"invite",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"handle",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"subscribers_count",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"verification",storageKey:null},{alias:null,args:null,concreteType:"XWA2NewsletterThreadSettings",kind:"LinkedField",name:"settings",plural:!1,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterReactionCodesSetting",kind:"LinkedField",name:"reaction_codes",plural:!1,selections:[d],storageKey:null}],storageKey:null},{condition:"fetch_wamo_sub",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterWamoSubMetadata",kind:"LinkedField",name:"wamo_sub",plural:!1,selections:[{alias:null,args:null,kind:"ScalarField",name:"plan_id",storageKey:null}],storageKey:null}]},{condition:"fetch_pinned_messages",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterPinnedMessage",kind:"LinkedField",name:"pinned_messages",plural:!0,selections:[{alias:null,args:null,kind:"ScalarField",name:"message_id",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"expiry_ts",storageKey:null}],storageKey:null}]}],storageKey:null},{condition:"fetch_viewer_metadata",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterViewerMetadata",kind:"LinkedField",name:"viewer_metadata",plural:!1,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterUserSetting",kind:"LinkedField",name:"settings",plural:!0,selections:[s,d],storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"role",storageKey:null},{condition:"fetch_wamo_sub",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,kind:"ScalarField",name:"wamo_sub_status",storageKey:null}]}],storageKey:null}]},{condition:"fetch_status_metadata",kind:"Condition",passingValue:!0,selections:[{alias:null,args:null,concreteType:"XWA2NewsletterStatusMetadata",kind:"LinkedField",name:"status_metadata",plural:!1,selections:[{alias:null,args:null,kind:"ScalarField",name:"last_status_server_id",storageKey:null},{alias:null,args:null,kind:"ScalarField",name:"last_status_sent_time",storageKey:null}],storageKey:null}]}],storageKey:null}];return{fragment:{argumentDefinitions:[e,t,n,r,o,a,i],kind:"Fragment",metadata:null,name:"WAWebMexFetchNewsletterJobQuery",selections:m,type:"Query",abstractKey:null},kind:"Request",operation:{argumentDefinitions:[i,o,t,e,a,r,n],kind:"Operation",name:"WAWebMexFetchNewsletterJobQuery",selections:m},params:{id:"27456920720571478",metadata:{},name:"WAWebMexFetchNewsletterJobQuery",operationKind:"query",text:null}}})();a.exports=e}),null); diff --git a/tests/conformance/2.3000.1047483476/provenance.json b/tests/conformance/2.3000.1047483476/provenance.json new file mode 100644 index 00000000..c7f6cb65 --- /dev/null +++ b/tests/conformance/2.3000.1047483476/provenance.json @@ -0,0 +1,99 @@ +{ + "waVersion": "2.3000.1047483476", + "setHash": "05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b", + "bundleCount": 579, + "bundleBytes": 75824128, + "modules": [ + { + "module": "WASmaxGroupsAcceptGroupAddRPC", + "bundleSha256": "34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2", + "start": 5922, + "end": 7533, + "sourceSha256": "2167e222e0875e85276bd519e5bd86839ba1e92808af5a1320b424d84ca3d7ae" + }, + { + "module": "WASmaxGroupsSetSubjectRPC", + "bundleSha256": "65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55", + "start": 28590, + "end": 29826, + "sourceSha256": "ddca755916395cc8f7b5d9cc1586e61030e0aad4f77e65385f86a09a47a3938d" + }, + { + "module": "WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess", + "bundleSha256": "34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2", + "start": 4229, + "end": 4728, + "sourceSha256": "3e137254ccda2e5b4b5d97fd512fe1ff74449cb360886195ecd46e098956718a" + }, + { + "module": "WASmaxInGroupsAcceptGroupAddResponseSuccess", + "bundleSha256": "34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2", + "start": 5098, + "end": 5459, + "sourceSha256": "2881a4c961499317969ca0c9a4ddbbd41cc60f4c0ac6567467e4ffc9cce62354" + }, + { + "module": "WASmaxInGroupsIQResultResponseMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 107120, + "end": 107921, + "sourceSha256": "5bb1d7b81b3b9658b04c755236384c73e1e8d4f7bffecd2718d473738b3f387d" + }, + { + "module": "WASmaxInGroupsSetSubjectResponseSuccess", + "bundleSha256": "65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55", + "start": 27492, + "end": 27845, + "sourceSha256": "0e40e8f7578b1ef63af1cd3aa9a67de5c161b02faaa7101f716bbb1dc68fa8e8" + }, + { + "module": "WASmaxOutGroupsAcceptGroupAddRequest", + "bundleSha256": "34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2", + "start": 5461, + "end": 5920, + "sourceSha256": "5cf14463914ca9f49171d4e081e1addbfcb1c061a0c4b87e0a8325dad6240c82" + }, + { + "module": "WASmaxOutGroupsBaseIQSetRequestMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 1138342, + "end": 1138644, + "sourceSha256": "66d7fb959f7fd555178fb69e4a8d5223cd87b5e010aee7c0c14310bd7f5aaa90" + }, + { + "module": "WASmaxOutGroupsBaseSetGroupMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 1138646, + "end": 1139064, + "sourceSha256": "0a5176779f9969f702da985eef658a46d846d9bf355f709ce58f7490d0d33fc7" + }, + { + "module": "WASmaxOutGroupsSetSubjectChangeSubjectMixin", + "bundleSha256": "65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55", + "start": 27847, + "end": 28186, + "sourceSha256": "3237a975959ca40026f296626e39cf6310eb57288f0cb9a79ce97edbc5714dec" + }, + { + "module": "WASmaxOutGroupsSetSubjectRequest", + "bundleSha256": "65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55", + "start": 28188, + "end": 28588, + "sourceSha256": "b5f5dfc3427e0eadc42c98fd1b43e20e50451c8a03c70f6ef941353899de746c" + }, + { + "module": "WAWebMexFetchNewsletterJob", + "bundleSha256": "f588ee2f033b87e018a3be34a37ec1347d8b6b8700de539e6be31481b29c4690", + "start": 2765650, + "end": 2766687, + "sourceSha256": "67d888be41361e8443f0c13795cc1485e325923909a76a37308eeeebbf8e9d4a" + }, + { + "module": "WAWebMexFetchNewsletterJobQuery.graphql", + "bundleSha256": "f588ee2f033b87e018a3be34a37ec1347d8b6b8700de539e6be31481b29c4690", + "start": 2760559, + "end": 2765648, + "sourceSha256": "c45101869de13508fddd0c7d0f28289079d4835ea6f4919ec5d1564668288458" + } + ] +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md new file mode 100644 index 00000000..8562f11f --- /dev/null +++ b/tests/conformance/README.md @@ -0,0 +1,179 @@ +# Independent conformance baseline + +This baseline checks extraction against reviewed Web source for SetSubject, +AcceptGroupAdd and FetchNewsletter. It adds no consumer dependency or runtime +API. It does not qualify the reference Rust emitter or the server protocol. + +The IQ test extracts the captured modules afresh and compares the resulting +contract with hand-reviewed expectations. It also checks the committed IQ IR. +Its response model supports only the selected success assertions. An additional +success field or unknown assertion needs an explicit test extension. This is +static verification, not execution of the JavaScript builders or parsers. + +## Evidence + +Captured from the exact `bundle-store` assets for these commits. Both manifests +use schema version 4.3.0. No account, credentials or session data were used. + +| Snapshot | Repository commit | Bundles | Uncompressed JS bytes | +| --- | --- | ---: | ---: | +| 2.3000.1045368834 | `1a441f0329c941fcdb238490a6c604550d8a9939` | 516 | 77,347,149 | +| 2.3000.1047483476 | `1f5167c4cebf263edaf001e06a2497f213c55793` | 579 | 75,824,128 | + +The old setHash is +`99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00`. +The current setHash is +`05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b`. + +The asset name is `bundles--.tar.xz` under +[the bundle-store release](https://github.com/oxidezap/whatspec/releases/tag/bundle-store). +The measured archive SHA-256 digests are, respectively: + +- `2edd4b3f8dae50b503e0b15920adc774e2680cdb542396d57608bb0374196ee1` +- `22243b9c2ff18a66cea8fd66551821fe0f9f09786d107e2be8284637db86c349` + +Each snapshot's `provenance.json` records all selected definitions, bundle SHA-256, +byte start/end with exclusive end, and source hash. The fixture bytes are the +exact AST span plus `;\n`. The capture verifies the complete JS multiset against +the lock, including multiplicity, size, bundle count and recomputed setHash. +It rejects missing modules and divergent duplicate definitions before writing. +It uses `wa_transform::extract_module_definitions`, never eval or vm. + +Only module boundaries share infrastructure with extraction. Expected protocol +facts below come from reviewing the captured source, not the extracted IR or +emitter. This does not independently verify the AST parser itself. + +## Reviewed expectations + +| Source path | Observation | Checks | +| --- | --- | --- | +| BaseSetGroupMixin -> BaseIQSetRequestMixin | `GROUP_JID(iqTo)`, namespace `w:g2`, type `set` | Both request contracts retain target, argument path, namespace and type | +| SetSubjectRequest -> ChangeSubjectMixin | `subjectElementValue` is `` content | Dynamic content argument survives extraction | +| AcceptGroupAddRequest | `acceptCode`, `acceptExpiration`, `acceptAdmin` become code, expiration, admin through CUSTOM_STRING, INT, USER_JID | Required attributes, types and argument paths | +| IQResultResponseMixin | Tag iq, type result, id equals request.id, from equals request.to | Missing, null and mismatched attributes, wrong tag, missing correlation context | +| AcceptGroupAddRPC | Approval success, bare success, client error, server error, in that order | Outcome order, child-present and childless successes, mutations removing gate or reversing successes | +| AcceptGroupAddResponseGroupJoinRequestSuccess | `flattenedChildWithTag(..., "membership_approval_request")` must succeed | Child-presence gate retained | +| SetSubjectResponseSuccess / AcceptGroupAddResponseSuccess | No payload child required and no extra-child rejection in these wrappers | Legitimate empty result, unrelated extension child | + +The success values still contain the pinned `type` field. “Empty success” means +no payload child, not permission to drop envelope validation or correlation. + +The small response model does not implement nested child flattening, helper +coercions, request builder execution, binary encoding, error payload parsing, +transport, timing or server behavior. It does not establish rejection rules for +invalid JavaScript builder arguments. Error outcome *order* is covered; the +error parser dependency closure is deliberately outside this capture. It does +not prove that a generated Rust parser enforces these constraints. Full +qualification remains pending integration with the other workstreams. + +## MEX triage + +The complete captured FetchNewsletter job and GraphQL module are identical in +both snapshots. `fetch_full_image` and coerced flags are always present; +`fetch_viewer_metadata` is conditional because a bare property read can be +undefined; `fetch_pinned_messages` remains undetermined because its external +call is not analyzed. Nested input.type is always present while input.key and +input.view_role can be omitted. + +All these flag variables have inferred boolean type from Relay conditions. +That type does not prove nullability or presence. Synthetic callers of the real +GraphQL operation separately test null, false, undefined, a property read, a +comparison and an unresolved call. Null survives JSON object-key serialization; +undefined does not. These fixtures do not claim the server accepts null for a +boolean variable. No concrete whatspec MEX defect was found in these cases, +and no MEX production implementation was changed. + +## Run and recapture + +Run from the repository root: + +```sh +cargo test -p wa-scan -p wa-mex --test independent_conformance +python3 -m unittest discover -s tests/conformance -p 'test_*.py' +cargo fmt --all -- --check +``` + +The two Python tests check fixture span/hash identity and rejection of changed, +missing, extra bundles, wrong setHash and wrong bundle count. They need no +network or compiled extractor. The Rust tests use only committed fixtures. + +To review a new snapshot, first restore its exact lock from bundle-store, then: + +```sh +cargo build -p wa-transform --example module_spans +python3 tests/conformance/capture.py LOCK_JSON VERIFIED_BUNDLE_DIRECTORY NEW_CAPTURE_DIRECTORY +``` + +`NEW_CAPTURE_DIRECTORY` must not exist. `capture.py` expects the example at +`target/debug/examples/module_spans`. If using a custom Cargo target directory, +copy that executable there or build the example with the default target first. +Review all changed definitions and dependency paths before changing expected +outcomes. The raw substring prefilter only selects candidate bundle files; +AST module definitions supply the actual byte spans. + +The 2026-10-07 cloud executor's `whatspec restore` failed with +`invalid peer certificate: UnknownIssuer`. Downloading the *same* release asset +with curl's active TLS validation succeeded. Every extracted JS hash and size +was then checked against the committed lock. TLS verification was not disabled. + +## Before/after evidence + +The test `source_derived_contracts_hold_across_two_verified_snapshots` was copied, +with these fixtures, into an isolated worktree at +`4e4c50a39b0b7d4e94a79697ee2d334d7e391c90`, before the child-gate fix. +It fails on the bare AcceptGroupAdd result: + +```text +left: Some("AcceptGroupAddResponseGroupJoinRequestSuccess") +right: Some("AcceptGroupAddResponseSuccess") +``` + +The same test passes at baseline HEAD `1f5167c4cebf263edaf001e06a2497f213c55793`. +Use a distinct `CARGO_TARGET_DIR` for each worktree when reproducing this check; +sharing one target across these same-version worktrees caused stale build +artifacts during the initial experiment. The final checks use separate targets. +The permanent mutation test also catches lost child gates and reordered outcomes. + +## Update effort and generation cost + +All 13 captured module definitions are byte-identical between the two snapshots. +All 13 provenance locations changed. Updating this selected baseline required +zero semantic expectation changes and zero source overrides. The two fixture +sets, including provenance, occupy 16,901 and 16,905 bytes. This observation does +not establish stability of the rest of either bundle or eliminate future review. + +Generation used baseline HEAD, the release-profile CLI, rustc 1.99.0, x86_64 +Linux, offline restored bundles, and separate initially empty output directories. +Times are individual wall-clock observations on a shared cloud executor; the +first run overlapped a test build. They are not performance claims. Build time, +download time and archive decompression are excluded. + +| Snapshot | First generation | Repeat generation | `--check` | Output files | Output bytes | +| --- | ---: | ---: | ---: | ---: | ---: | +| 2.3000.1045368834 | 14.060 s | 11.853 s | 11.999 s | 35 | 14,750,300 | +| 2.3000.1047483476 | 12.616 s | 12.872 s | 11.801 s | 35 | 14,828,437 | + +All 35 files matched byte-for-byte between independent generations for each +snapshot. The output size increased by 78,137 bytes. The CLI's `--check` covers +27 committed artifacts; it also passed against the current committed tree. +All 12 schema/document pairs passed validation for the committed snapshot and +both regenerated outputs. Reference Rust output is included in the 35-file +comparison but was not compiled against a runtime consumer. + +Reproduce with the release CLI, explicit version and empty output paths: + +```sh +cargo build --release -p whatspec +time target/release/whatspec update --bundles BUNDLES --wa-version VERSION --out OUTPUT_1 +time target/release/whatspec update --bundles BUNDLES --wa-version VERSION --out OUTPUT_2 +diff -qr OUTPUT_1 OUTPUT_2 +time target/release/whatspec update --bundles BUNDLES --wa-version VERSION --out OUTPUT_1 --check +python3 scripts/validate-schemas.py OUTPUT_1 +``` + +Current lint remains red on six *improved* exact baselines: unresolved IQ +attribute paths 49 vs 55, child paths 10 vs 12, MEX operations without established +presence 10 vs 13, undetermined MEX variables 106 vs 108, WAM constructions with +no catalog entry 29 vs 41, and unread WAM arguments 93 vs 104. Baselines and CI +were not changed here. Published-SHA CI and combined emitter qualification are +still required before declaring the repository qualified. diff --git a/tests/conformance/capture.py b/tests/conformance/capture.py new file mode 100644 index 00000000..0dbc165a --- /dev/null +++ b/tests/conformance/capture.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Verify a complete locked bundle set, then capture named modules by AST spans. + +Usage: python3 tests/conformance/capture.py LOCK BUNDLES OUTPUT +Build first: cargo build -p wa-transform --example module_spans +No bundle code is executed. OUTPUT must not already exist. +""" +import collections +import hashlib +import json +from pathlib import Path +import subprocess +import sys + +MODULES = [ + 'WASmaxOutGroupsSetSubjectRequest', + 'WASmaxOutGroupsSetSubjectChangeSubjectMixin', + 'WASmaxOutGroupsBaseSetGroupMixin', + 'WASmaxOutGroupsBaseIQSetRequestMixin', + 'WASmaxOutGroupsAcceptGroupAddRequest', + 'WASmaxGroupsSetSubjectRPC', + 'WASmaxGroupsAcceptGroupAddRPC', + 'WASmaxInGroupsSetSubjectResponseSuccess', + 'WASmaxInGroupsAcceptGroupAddResponseSuccess', + 'WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess', + 'WASmaxInGroupsIQResultResponseMixin', + 'WAWebMexFetchNewsletterJob', + 'WAWebMexFetchNewsletterJobQuery.graphql', +] + + +def capture(lock_path, bundles, output): + lock = json.loads(lock_path.read_text()) + files = sorted(bundles.rglob('*.js')) + raw = [(p, p.read_bytes()) for p in files] + actual = collections.Counter((hashlib.sha256(b).hexdigest(), len(b)) for _, b in raw) + expected = collections.Counter((b['sha256'], b['size']) for b in lock['bundles']) + if actual != expected or len(files) != lock['bundleCount']: + raise ValueError('bundle multiset differs from lock') + fingerprint = hashlib.sha256('\n'.join(sorted( + f'{h}:{s}' for (h, s), n in actual.items() for _ in range(n) + )).encode()).hexdigest() + if fingerprint != lock['setHash']: + raise ValueError('setHash differs from lock') + executable = Path(__file__).resolve().parents[2] / 'target/debug/examples/module_spans' + captured = {} + evidence = [] + for path, data in raw: + if not any(('"' + name + '"').encode() in data for name in MODULES): + continue + spans = subprocess.check_output([str(executable), str(path), *MODULES], text=True) + for row in spans.splitlines(): + name, start, end = row.split('\t') + start, end = int(start), int(end) + source = data[start:end] + if name in captured and captured[name] != source: + raise ValueError(f'divergent duplicate module: {name}') + captured[name] = source + evidence.append(dict(module=name, bundleSha256=hashlib.sha256(data).hexdigest(), + start=start, end=end, sourceSha256=hashlib.sha256(source).hexdigest())) + if set(captured) != set(MODULES): + raise ValueError(f'missing modules: {set(MODULES) - set(captured)}') + output.mkdir(parents=True, exist_ok=False) + for name, source in sorted(captured.items()): + (output / (name + '.js')).write_bytes(source + b';\n') + (output / 'provenance.json').write_text(json.dumps(dict( + waVersion=lock['waVersion'], setHash=fingerprint, + bundleCount=len(files), bundleBytes=sum(len(b) for _, b in raw), + modules=sorted(evidence, key=lambda e: (e['module'], e['bundleSha256'], e['start'])) + ), indent=2) + '\n') + print(f"verified {len(files)} bundles; captured {len(captured)} modules, {len(evidence)} definitions") + + +if __name__ == '__main__': + capture(*(Path(arg) for arg in sys.argv[1:])) diff --git a/tests/conformance/test_capture.py b/tests/conformance/test_capture.py new file mode 100644 index 00000000..a668c330 --- /dev/null +++ b/tests/conformance/test_capture.py @@ -0,0 +1,54 @@ +"""Offline checks for capture identity and corrupted input rejection.""" +import hashlib +import json +from pathlib import Path +import tempfile +import unittest + +from capture import MODULES, capture + + +class CaptureTests(unittest.TestCase): + def test_committed_sources_match_every_recorded_span_hash(self): + for version in ['2.3000.1045368834', '2.3000.1047483476']: + root = Path(__file__).parent / version + evidence = json.loads((root / 'provenance.json').read_text()) + self.assertEqual({e['module'] for e in evidence['modules']}, set(MODULES)) + for entry in evidence['modules']: + source = (root / (entry['module'] + '.js')).read_bytes() + self.assertTrue(source.endswith(b';\n')) + source = source[:-2] # Capture's terminator, not part of the AST span. + self.assertEqual(len(source), entry['end'] - entry['start']) + self.assertEqual(hashlib.sha256(source).hexdigest(), entry['sourceSha256']) + + def test_changed_missing_extra_and_wrong_set_hash_are_rejected_before_ast(self): + for failure in ['changed', 'missing', 'extra', 'setHash', 'count']: + with self.subTest(failure=failure), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + bundles = root / 'bundles' + bundles.mkdir() + source = b'fixture' + digest = hashlib.sha256(source).hexdigest() + lock = dict(waVersion='fixture', bundleCount=1, + setHash=hashlib.sha256(f'{digest}:7'.encode()).hexdigest(), + bundles=[dict(sha256=digest, size=7)]) + (bundles / 'input.js').write_bytes(source) + if failure == 'changed': + (bundles / 'input.js').write_bytes(b'changed') + elif failure == 'missing': + (bundles / 'input.js').unlink() + elif failure == 'extra': + (bundles / 'extra.js').write_bytes(source) + elif failure == 'setHash': + lock['setHash'] = '0' * 64 + else: + lock['bundleCount'] = 2 + lock_path = root / 'lock.json' + lock_path.write_text(json.dumps(lock)) + with self.assertRaises(ValueError): + capture(lock_path, bundles, root / 'output') + self.assertFalse((root / 'output').exists()) + + +if __name__ == '__main__': + unittest.main() From 0439f9ad5b17b85b457d327668eb805e20bd9ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 21:44:47 +0000 Subject: [PATCH 02/52] feat: add evidence-based snapshot contract review --- README.md | 9 + crates/whatspec/src/contract_diff.rs | 504 ++++++++++++++++++ crates/whatspec/src/main.rs | 15 +- crates/whatspec/src/source_index.rs | 127 +++++ docs/snapshot-quality-2026-10-07.md | 132 +++++ docs/snapshot-quality-2026-10-07.reviews.json | 23 + docs/snapshot-quality-2026-10-07.sources.json | 198 +++++++ docs/snapshot-quality.md | 108 ++++ scripts/lint-ir.py | 22 +- scripts/tests/test_snapshot_validation.py | 65 +++ scripts/validate-schemas.py | 27 +- 11 files changed, 1210 insertions(+), 20 deletions(-) create mode 100644 crates/whatspec/src/contract_diff.rs create mode 100644 crates/whatspec/src/source_index.rs create mode 100644 docs/snapshot-quality-2026-10-07.md create mode 100644 docs/snapshot-quality-2026-10-07.reviews.json create mode 100644 docs/snapshot-quality-2026-10-07.sources.json create mode 100644 docs/snapshot-quality.md create mode 100644 scripts/tests/test_snapshot_validation.py diff --git a/README.md b/README.md index f6f381ef..ea3fdf0b 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ cargo run --release -p whatspec -- restore --wasm --from-lock generated/wasm.loc # Compare two generated outputs (e.g. across a WhatsApp version bump): cargo run --release -p whatspec -- diff old-generated/ generated/ +# Full contract deltas, hashed source pointers, and evidence classifications: +cargo run --release -p whatspec -- diff old-generated/ generated/ --json # Deterministically reproduce & verify generated/ from its pinned inputs (no live fetch): ./scripts/regen.sh @@ -197,3 +199,10 @@ A Rust consumer can instead use the committed reference modules directly; they d ## License MIT © 2025 João Lucas de Oliveira Lopes — see [LICENSE](LICENSE). + +### Reviewing snapshot quality + +Use the [snapshot quality workflow](docs/snapshot-quality.md) to compare full contracts, +locate operations in verified bundles, and record evidence for upstream or extraction +changes. The [September snapshot investigation](docs/snapshot-quality-2026-10-07.md) +explains why falling lint counts alone cannot justify lowering a baseline. diff --git a/crates/whatspec/src/contract_diff.rs b/crates/whatspec/src/contract_diff.rs new file mode 100644 index 00000000..8eb163c7 --- /dev/null +++ b/crates/whatspec/src/contract_diff.rs @@ -0,0 +1,504 @@ +//! Reviewable contract deltas. Neither a smaller catalog nor matching shapes prove +//! a rename, extraction improvement, or an upstream protocol removal. +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Component, Path}; + +use anyhow::{Context, Result, bail, ensure}; +use serde_json::{Value, json}; + +const FORMAT_VERSION: u32 = 1; + +fn pointer(key: &str) -> String { + key.replace('~', "~0").replace('/', "~1") +} + +fn local_file(root: &Path, rel: &str) -> Result> { + ensure!( + !rel.is_empty() + && Path::new(rel) + .components() + .all(|c| matches!(c, Component::Normal(_))), + "invalid snapshot-relative path: {rel}" + ); + std::fs::read(root.join(rel)).with_context(|| format!("read {rel}")) +} + +struct Snapshot { + manifest: Value, + lock: wa_store::lock::BundleLock, + files: BTreeMap, +} + +impl Snapshot { + fn read(root: &Path) -> Result { + let manifest = super::read_json(&root.join("manifest.json"))?; + let lock: wa_store::lock::BundleLock = + serde_json::from_slice(&local_file(root, "bundles.lock.json")?)?; + lock.verify_self_consistent().map_err(anyhow::Error::msg)?; + ensure!( + manifest["waVersion"].as_str() == Some(&lock.wa_version), + "manifest/lock waVersion mismatch" + ); + let domains = manifest["domains"] + .as_object() + .context("manifest.domains missing")?; + ensure!(!domains.is_empty(), "manifest.domains empty"); + let mut files = BTreeMap::new(); + for (domain, entry) in domains { + let rel = entry["file"].as_str().context("domain file missing")?; + let bytes = local_file(root, rel)?; + let hash = wa_text::sha256_hex(&bytes); + ensure!( + entry["sha256"].as_str() == Some(&hash), + "{rel}: manifest hash mismatch" + ); + let value = if rel.ends_with(".json") { + let value: Value = + serde_json::from_slice(&bytes).with_context(|| format!("parse {rel}"))?; + ensure!( + value["waVersion"] == manifest["waVersion"], + "{rel}: waVersion mismatch" + ); + ensure!( + value["schemaVersion"] == manifest["schemaVersion"], + "{rel}: schemaVersion mismatch" + ); + value + } else { + // Protobuf is reported as an opaque artifact, never a parsed contract. + json!({"artifactSha256": hash}) + }; + files.insert(domain.clone(), (rel.to_string(), hash, value)); + } + Ok(Self { + manifest, + lock, + files, + }) + } + + fn metadata(&self) -> Value { + json!({"waVersion": self.manifest["waVersion"], "schemaVersion": self.manifest["schemaVersion"], + "generatorVersion": self.manifest["generatorVersion"], "setHash": self.lock.set_hash, + "bundleCount": self.lock.bundle_count}) + } +} + +/// Only explicit collection identities. All other arrays retain order and +/// multiplicity. In particular, parser variant order is part of the contract. +fn identity_keys(domain: &str, collection: &str) -> Option<&'static [&'static str]> { + match (domain, collection) { + ("enums", "enums") | ("wam", "enums") => Some(&["module", "name"]), + ("abprops", "configs") => Some(&["module", "name"]), + ("iq", "stanzas") | ("stanza", "stanzas") => Some(&["moduleName", "exportedFunction"]), + ("incoming", "incoming") => Some(&["module", "tag"]), + ("srvreq", "requests") => Some(&["module", "tag"]), + ("notif", "notifications") => Some(&["type"]), + ("notif", "stanzaTags") => Some(&["tag"]), + ("wam", "events") => Some(&["module", "name"]), + ("wam", "globals") | ("wam", "constants") => Some(&["name"]), + ("wam", "privateStatsIds") => Some(&["key"]), + ("wasm", "binaries") => Some(&["name"]), + ("wasm", "resources") => Some(&["bxId"]), + _ => None, + } +} + +type KeyedItems<'a> = BTreeMap>; + +fn keyed<'a>(value: &'a Value, keys: &[&str]) -> Option> { + let mut map: KeyedItems<'a> = BTreeMap::new(); + for (index, item) in value.as_array()?.iter().enumerate() { + let identity: Vec<&Value> = keys + .iter() + .map(|key| item.get(*key)) + .collect::>()?; + let identity = serde_json::to_string(&identity).ok()?; + map.entry(identity).or_default().push((index, item)); + } + Some(map) +} + +fn walk( + old: Option<&Value>, + new: Option<&Value>, + old_path: &str, + new_path: &str, + changes: &mut Vec, +) { + if old == new { + return; + } + if let (Some(Value::Object(a)), Some(Value::Object(b))) = (old, new) { + for key in a.keys().chain(b.keys()).collect::>() { + walk( + a.get(key), + b.get(key), + &format!("{old_path}/{}", pointer(key)), + &format!("{new_path}/{}", pointer(key)), + changes, + ); + } + } else if let (Some(Value::Array(a)), Some(Value::Array(b))) = (old, new) { + // Preserve an array as one change: a deletion must not be misrepresented + // as a series of field edits on every later element. + changes.push(json!({"oldPath": old_path, "newPath": new_path, "kind": "ordered-array-changed", "before": a, "after": b})); + } else { + changes.push(json!({"oldPath": old.map(|_| old_path), "newPath": new.map(|_| new_path), + "kind": if old.is_none() { "added" } else if new.is_none() { "removed" } else { "changed" }, + "before": old, "after": new})); + } +} + +fn document_diff(domain: &str, old: &Value, new: &Value) -> Vec { + let mut changes = Vec::new(); + let (Some(a), Some(b)) = (old.as_object(), new.as_object()) else { + walk(Some(old), Some(new), "", "", &mut changes); + return changes; + }; + for collection in a.keys().chain(b.keys()).collect::>() { + if matches!(collection.as_str(), "schemaVersion" | "waVersion") { + continue; + } + let path = format!("/{}", pointer(collection)); + let pair = identity_keys(domain, collection).and_then(|keys| { + Some(( + keyed(a.get(collection)?, keys)?, + keyed(b.get(collection)?, keys)?, + )) + }); + if let Some((old_items, new_items)) = pair { + for identity in old_items + .keys() + .chain(new_items.keys()) + .collect::>() + { + let o = old_items + .get(identity) + .map(Vec::as_slice) + .unwrap_or_default(); + let n = new_items + .get(identity) + .map(Vec::as_slice) + .unwrap_or_default(); + let start = changes.len(); + if o.len() <= 1 && n.len() <= 1 { + walk( + o.first().map(|(_, v)| *v), + n.first().map(|(_, v)| *v), + &o.first() + .map(|(i, _)| format!("{path}/{i}")) + .unwrap_or_default(), + &n.first() + .map(|(i, _)| format!("{path}/{i}")) + .unwrap_or_default(), + &mut changes, + ); + } else { + let before: Vec<_> = o.iter().map(|(_, v)| *v).collect(); + let after: Vec<_> = n.iter().map(|(_, v)| *v).collect(); + if before != after { + changes.push(json!({"oldPath":path,"newPath":path,"kind":"ambiguous-identity-group-changed", + "before":before,"after":after, + "oldIndices":o.iter().map(|(i,_)| i).collect::>(), + "newIndices":n.iter().map(|(i,_)| i).collect::>(), + "identityDiagnostic":"non-unique identity; occurrences compared as an ordered group"})); + } + } + for change in &mut changes[start..] { + change["identity"] = + serde_json::from_str(identity).expect("serialized identity"); + } + } + } else { + let start = changes.len(); + walk( + a.get(collection), + b.get(collection), + &path, + &path, + &mut changes, + ); + if identity_keys(domain, collection).is_some() { + for change in &mut changes[start..] { + change["identityDiagnostic"] = + json!("missing or non-unique identity; collection compared in order"); + } + } + } + } + changes +} + +fn reference(file: Option<&(String, String, Value)>, path: &Value) -> Value { + let Some((rel, hash, document)) = file else { + return Value::Null; + }; + let Some(path) = path.as_str() else { + return Value::Null; + }; + // The nearest explicit source name is a locator, not proof that this module + // supplies this specific field: composed shapes can come from dependencies. + let mut at = path.to_string(); + let mut module = Value::Null; + loop { + if let Some(node) = document.pointer(&at) { + for key in ["module", "moduleName", "handlerModule"] { + if let Some(name) = node.get(key).and_then(Value::as_str) { + module = json!(name); + break; + } + } + } + if !module.is_null() || at.is_empty() { + break; + } + at.truncate(at.rfind('/').unwrap_or(0)); + } + json!({"file": rel, "sha256": hash, "pointer": path, "sourceModuleHint": module, + "sourceStatus": "requires-source-review"}) +} + +pub fn report(old: &Path, new: &Path, evidence: Option<&Path>) -> Result { + let old = Snapshot::read(old)?; + let new = Snapshot::read(new)?; + let mut changes = Vec::new(); + for domain in old + .files + .keys() + .chain(new.files.keys()) + .collect::>() + { + let a = old.files.get(domain); + let b = new.files.get(domain); + let deltas = match (a, b) { + (Some((_, _, av)), Some((_, _, bv))) => document_diff(domain, av, bv), + _ => { + let mut d = Vec::new(); + walk(a.map(|v| &v.2), b.map(|v| &v.2), "", "", &mut d); + d + } + }; + for mut change in deltas { + change["domain"] = json!(domain); + change["oldSource"] = reference(a, &change["oldPath"]); + change["newSource"] = reference(b, &change["newPath"]); + // Bind a review to exact artifact contents AND inputs, not just a name + // that can survive an unrelated future snapshot. + change["oldSetHash"] = json!(old.lock.set_hash); + change["newSetHash"] = json!(new.lock.set_hash); + change["id"] = json!(wa_text::sha256_hex( + serde_json::to_string(&change)?.as_bytes() + )); + change["assessment"] = json!({"classification": "indeterminate", "basis": "Contract delta only; inspect pinned sources and same-input regeneration."}); + changes.push(change); + } + } + if let Some(path) = evidence { + apply_reviews(&mut changes, &super::read_json(path)?)?; + } + let mut classifications: BTreeMap = BTreeMap::new(); + for change in &changes { + *classifications + .entry( + change["assessment"]["classification"] + .as_str() + .unwrap() + .to_string(), + ) + .or_default() += 1; + } + Ok( + json!({"reportVersion": FORMAT_VERSION, "old": old.metadata(), "new": new.metadata(), + "sameInputs": old.lock.set_hash == new.lock.set_hash, "classifications": classifications, + "changes": changes, "oldDiagnostics": old.manifest["diagnostics"], "newDiagnostics": new.manifest["diagnostics"], + "limits": ["No automatic rename matching or compatibility verdict.", "Reviews are supplied evidence assessments, not machine proofs.", + "Source module hints may require following mixins or other dependencies.", "Unkeyed and ambiguous arrays retain order; protobuf is an opaque artifact delta."]}), + ) +} + +fn apply_reviews(changes: &mut [Value], reviews: &Value) -> Result<()> { + ensure!( + reviews["reportVersion"] == FORMAT_VERSION, + "unsupported review reportVersion" + ); + let entries = reviews["reviews"] + .as_array() + .context("reviews must be an array")?; + let mut seen = BTreeSet::new(); + for entry in entries { + let id = entry["id"].as_str().context("review id missing")?; + ensure!(seen.insert(id), "duplicate review id {id}"); + let change = changes + .iter_mut() + .find(|c| c["id"] == id) + .with_context(|| format!("stale or unknown review {id}"))?; + let label = entry["classification"] + .as_str() + .context("classification missing")?; + ensure!( + matches!( + label, + "upstream-change" | "extraction-improvement" | "extraction-loss" | "indeterminate" + ), + "unknown classification {label}" + ); + ensure!( + entry["basis"] + .as_str() + .is_some_and(|s| !s.trim().is_empty()), + "review basis missing" + ); + let refs = entry["references"] + .as_array() + .context("review references missing")?; + ensure!( + !refs.is_empty() + && refs + .iter() + .all(|v| v.as_str().is_some_and(|s| !s.trim().is_empty())), + "review needs recoverable source references" + ); + change["assessment"] = json!({"classification": label, "basis": entry["basis"], "references": refs, "origin": "reviewed-evidence"}); + } + Ok(()) +} + +pub fn run(args: &[String]) -> Result<()> { + ensure!( + args.len() == 3 || args.len() == 5, + "usage: whatspec diff --json [--evidence ]" + ); + ensure!(args[2] == "--json", "expected --json"); + let evidence = if args.len() == 5 { + if args[3] != "--evidence" { + bail!("expected --evidence"); + } + Some(Path::new(&args[4])) + } else { + None + }; + println!( + "{}", + serde_json::to_string_pretty(&report(Path::new(&args[0]), Path::new(&args[1]), evidence)?)? + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn snapshot_hashes_and_versions_are_checked_before_diffing() { + let dir = + std::env::temp_dir().join(format!("whatspec-contract-diff-{}", std::process::id())); + std::fs::create_dir_all(dir.join("mex")).unwrap(); + let lock = wa_store::lock::BundleLock::new("test", vec![]); + std::fs::write(dir.join("bundles.lock.json"), lock.to_pretty_json()).unwrap(); + let doc = + json!({"waVersion":"test", "schemaVersion":"4.3.0", "operations": {"A":{"docId":"1"}}}) + .to_string(); + std::fs::write(dir.join("mex/index.json"), &doc).unwrap(); + let mut manifest = json!({"waVersion":"test", "schemaVersion":"4.3.0", "domains":{"mex":{"file":"mex/index.json","sha256":wa_text::sha256_hex(doc.as_bytes())}}}); + std::fs::write(dir.join("manifest.json"), manifest.to_string()).unwrap(); + assert!( + report(&dir, &dir, None).unwrap()["changes"] + .as_array() + .unwrap() + .is_empty() + ); + std::fs::write(dir.join("mex/index.json"), "{}").unwrap(); + assert!( + report(&dir, &dir, None) + .unwrap_err() + .to_string() + .contains("hash mismatch") + ); + std::fs::write(dir.join("mex/index.json"), &doc).unwrap(); + manifest["waVersion"] = json!("other"); + std::fs::write(dir.join("manifest.json"), manifest.to_string()).unwrap(); + assert!( + report(&dir, &dir, None) + .unwrap_err() + .to_string() + .contains("waVersion mismatch") + ); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn reviewed_causes_are_explicit_and_cannot_be_renames() { + let mut changes = [json!({"id":"current"})]; + let mut review = json!({"reportVersion":1,"reviews":[{"id":"current","classification":"extraction-loss","basis":"field remains in source","references":["bundle SHA-256 and byte span"]}]}); + apply_reviews(&mut changes, &review).unwrap(); + assert_eq!(changes[0]["assessment"]["origin"], "reviewed-evidence"); + review["reviews"][0]["classification"] = json!("rename"); + assert!(apply_reviews(&mut changes, &review).is_err()); + review["reviews"][0]["classification"] = json!("extraction-loss"); + review["reviews"][0]["references"] = json!([]); + assert!(apply_reviews(&mut changes, &review).is_err()); + } + + #[test] + fn same_name_enum_modules_do_not_collapse() { + let a = json!({"enums": [{"module":"A","name":"X","variants":[1]}, {"module":"B","name":"X","variants":[2]}]}); + let b = json!({"enums": [{"module":"B","name":"X","variants":[3]}, {"module":"A","name":"X","variants":[1]}]}); + let changes = document_diff("enums", &a, &b); + assert_eq!(changes.len(), 1); + assert_eq!(changes[0]["identity"], json!(["B", "X"])); + assert_eq!(changes[0]["oldPath"], "/enums/1/variants"); + assert_eq!(changes[0]["newPath"], "/enums/0/variants"); + } + + #[test] + fn duplicates_and_variant_order_are_not_erased() { + let a = json!({"enums": [{"module":"A","name":"X"}, {"module":"A","name":"X"}]}); + let b = json!({"enums": [{"module":"A","name":"X"}]}); + let changes = document_diff("enums", &a, &b); + assert_eq!(changes.len(), 1); + assert!(changes[0].get("identityDiagnostic").is_some()); + assert_eq!( + document_diff("iq", &json!({"variants":[1,2]}), &json!({"variants":[2,1]})).len(), + 1 + ); + } + + #[test] + fn absent_null_and_pointer_escapes_are_distinct() { + let changes = document_diff( + "mex", + &json!({"operations":{"A/B~C":null}}), + &json!({"operations":{}}), + ); + assert_eq!(changes[0]["kind"], "removed"); + assert_eq!(changes[0]["oldPath"], "/operations/A~1B~0C"); + assert!(changes[0]["newPath"].is_null()); + } + + #[test] + fn rename_is_add_remove_not_inferred_identity() { + let changes = document_diff( + "mex", + &json!({"operations":{"Old":{"docId":"1"}}}), + &json!({"operations":{"New":{"docId":"1"}}}), + ); + assert_eq!(changes.len(), 2); + assert!( + changes + .iter() + .all(|v| v["kind"] == "added" || v["kind"] == "removed") + ); + } + + #[test] + fn stale_reviews_fail_instead_of_reclassifying_another_snapshot() { + let review = json!({"reportVersion":1,"reviews":[{"id":"wrong","classification":"upstream-change","basis":"verified","references":["bundle:offset"]}]}); + assert!( + apply_reviews(&mut [json!({"id":"current"})], &review) + .unwrap_err() + .to_string() + .contains("stale") + ); + } +} diff --git a/crates/whatspec/src/main.rs b/crates/whatspec/src/main.rs index a41e7456..432acebf 100644 --- a/crates/whatspec/src/main.rs +++ b/crates/whatspec/src/main.rs @@ -2,6 +2,9 @@ //! versioned artifacts (IQ specs, WAProto.proto, mex operations, appstate //! schemas) to disk, ready to be committed — locally or from CI. +mod contract_diff; +mod source_index; + use std::collections::BTreeSet; // Only the fetch path builds the bootloader pin map from it. #[cfg(feature = "fetch")] @@ -51,6 +54,7 @@ fn main() -> Result<()> { Some("update") => update(&args[1..]), Some("mex-ids") => mex_ids(&args[1..]), Some("diff") => diff(&args[1..]), + Some("source-index") => source_index::run(&args[1..]), Some("restore") => restore_cmd(&args[1..]), _ => { eprintln!("{}", usage()); @@ -147,9 +151,11 @@ fn usage() -> String { Refreshes the rotating `id` of each `MexDoc` in a hand-curated mex_ids.rs by matching\n\ its stable `name` against the current bundle, preserving const names/grouping, and\n\ reports stale entries whose `name` no longer exists upstream.\n\n\ - whatspec diff \n\n\ + whatspec diff [--json [--evidence ]]\n\n\ Compares two generated output directories (by their `manifest.json` + `index.json`s)\n\ and prints version/count deltas and the namespaces/operations/actions added or removed.\n\n\ + whatspec source-index [module ...]\n\n\ + Writes a verified-bundle AST source locator index to stdout.\n\n\ whatspec restore {FLAG_FROM_LOCK} ({FLAG_OUT} | {FLAG_CACHE} )\n \ [{FLAG_WASM}] [{FLAG_ARCHIVE} ] [{FLAG_REPO} ]\n\n\ Rebuilds the exact bundle set a `generated/` snapshot was built from — pulled from the\n\ @@ -556,6 +562,13 @@ fn mex_ids(args: &[String]) -> Result<()> { /// `whatspec diff ` — report what changed between two /// generated outputs (version/count deltas + per-domain name set add/remove). fn diff(args: &[String]) -> Result<()> { + if args.iter().any(|arg| arg == "--json") { + return contract_diff::run(args); + } + anyhow::ensure!( + args.len() == 2, + "diff requires exactly two paths, or --json [--evidence ]" + ); let (old, new) = match (args.first(), args.get(1)) { (Some(a), Some(b)) => (Path::new(a), Path::new(b)), _ => anyhow::bail!("diff requires two paths: whatspec diff "), diff --git a/crates/whatspec/src/source_index.rs b/crates/whatspec/src/source_index.rs new file mode 100644 index 00000000..87d1521c --- /dev/null +++ b/crates/whatspec/src/source_index.rs @@ -0,0 +1,127 @@ +//! Optional provenance sidecar. Never executes vendor code or changes the IR. +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; + +use anyhow::{Context, Result, ensure}; +use serde_json::{Value, json}; +use wa_store::lock::{BundleId, BundleLock}; + +fn modules(source: &str, hash: &str, selected: &BTreeSet<&str>) -> Vec<(String, Value)> { + wa_transform::extract_module_definitions(source).into_iter() + .filter(|m| selected.is_empty() || selected.contains(m.name.as_str())) + .map(|m| { + let value = json!({"bundleSha256": hash, "start": m.start, "end": m.end, + "moduleSha256": wa_text::sha256_hex(&source.as_bytes()[m.start..m.end]), + "factorySha256": wa_text::sha256_hex(&source.as_bytes()[m.factory_start..m.factory_end]), + "dependencies": m.deps}); + (m.name, value) + }).collect() +} + +pub fn index(lock_path: &Path, bundles: &Path, selected: &BTreeSet<&str>) -> Result { + let lock: BundleLock = serde_json::from_slice(&std::fs::read(lock_path)?)?; + lock.verify_self_consistent().map_err(anyhow::Error::msg)?; + let mut files = std::fs::read_dir(bundles)? + .map(|entry| entry.map(|e| e.path())) + .collect::>>()?; + files.retain(|p| p.extension().is_some_and(|s| s == "js")); + files.sort(); + let mut identities = Vec::new(); + let mut definitions: BTreeMap> = BTreeMap::new(); + for path in files { + let bytes = std::fs::read(&path)?; + let hash = wa_text::sha256_hex(&bytes); + let source = std::str::from_utf8(&bytes) + .with_context(|| format!("{}: byte spans require valid UTF-8", path.display()))?; + identities.push(BundleId { + sha256: hash.clone(), + size: bytes.len() as u64, + url: None, + }); + for (name, definition) in modules(source, &hash, selected) { + definitions.entry(name).or_default().push(definition); + } + } + let observed = BundleLock::new(&lock.wa_version, identities); + ensure!( + observed.set_hash == lock.set_hash && observed.bundle_count == lock.bundle_count, + "bundle multiset differs from lock; source index not emitted" + ); + for definitions in definitions.values_mut() { + definitions.sort_by_key(|d| { + ( + d["bundleSha256"].as_str().unwrap().to_owned(), + d["start"].as_u64().unwrap(), + ) + }); + } + Ok( + json!({"sourceIndexVersion":1,"waVersion":lock.wa_version,"setHash":lock.set_hash, + "bundleCount":lock.bundle_count,"modules":definitions, + "coverage":"recovered-definitions-only", + "limits":["Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", + "The existing AST extractor does not expose parse diagnostics. Absence from this index is not proof of absence upstream.", + "All recovered occurrences are retained. Equal module names with different hashes need review.", + "Module provenance locates code; field-level semantics may require following dependencies."]}), + ) +} + +pub fn run(args: &[String]) -> Result<()> { + ensure!( + args.len() >= 2, + "usage: whatspec source-index [module ...]" + ); + let selected = args[2..].iter().map(String::as_str).collect(); + println!( + "{}", + serde_json::to_string_pretty(&index(Path::new(&args[0]), Path::new(&args[1]), &selected)?)? + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn source_index_refuses_missing_or_tampered_bundle_sets() { + let dir = + std::env::temp_dir().join(format!("whatspec-source-index-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let bytes = b"__d(\"Real\",[],function(){});"; + let lock = BundleLock::new( + "test", + vec![BundleId { + sha256: wa_text::sha256_hex(bytes), + size: bytes.len() as u64, + url: None, + }], + ); + let lock_path = dir.join("lock.json"); + std::fs::write(&lock_path, lock.to_pretty_json()).unwrap(); + assert!(index(&lock_path, &dir, &BTreeSet::new()).is_err()); + std::fs::write(dir.join("one.js"), bytes).unwrap(); + assert!(index(&lock_path, &dir, &BTreeSet::new()).is_ok()); + std::fs::write(dir.join("duplicate.js"), bytes).unwrap(); + assert!(index(&lock_path, &dir, &BTreeSet::new()).is_err()); + std::fs::remove_file(dir.join("duplicate.js")).unwrap(); + std::fs::write(dir.join("one.js"), b"tampered").unwrap(); + assert!(index(&lock_path, &dir, &BTreeSet::new()).is_err()); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn ast_locations_ignore_strings_and_keep_duplicate_definitions() { + let source = "// á UTF-8 prefix\nvar text = '__d(\"Fake\",[],function(){})'; __d(\"Real\",[\"Dep\"],function(){return 1;}); __d(\"Real\",[],function(){return 2;});"; + let result = modules(source, "bundle-hash", &BTreeSet::new()); + assert_eq!(result.len(), 2); + assert_eq!(result[0].0, "Real"); + let start = result[0].1["start"].as_u64().unwrap() as usize; + let end = result[0].1["end"].as_u64().unwrap() as usize; + assert_eq!( + &source[start..end], + "__d(\"Real\",[\"Dep\"],function(){return 1;})" + ); + assert_ne!(result[0].1["factorySha256"], result[1].1["factorySha256"]); + } +} diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md new file mode 100644 index 00000000..c31bec3b --- /dev/null +++ b/docs/snapshot-quality-2026-10-07.md @@ -0,0 +1,132 @@ +# September snapshot quality investigation + +The current remote HEAD checked on 2026-10-07 was +`1f5167c4cebf263edaf001e06a2497f213c55793`. The comparison baseline was +`1a441f0329c941fcdb238490a6c604550d8a9939`. The intervening commit changes only +16 generated files. Both use IR schema 4.3.0 and the same extractor source. +The reference research report supplied through Library was treated as a set of +claims to recheck. No other repository or real session was used. + +| Evidence | Old | New | +|---|---|---| +| WA version | 2.3000.1045368834 | 2.3000.1047483476 | +| Bundles | 516 | 579 | +| Set hash | `99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00` | `05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b` | +| Archive SHA-256 | `2edd4b3f8dae50b503e0b15920adc774e2680cdb542396d57608bb0374196ee1` | `22243b9c2ff18a66cea8fd66551821fe0f9f09786d107e2be8284637db86c349` | + +Both archives came from `oxidezap/whatspec` release `bundle-store`, named +`bundles--.tar.xz`. Restore verified each exact bundle +multiset. TLS in the Rust downloader reported `UnknownIssuer`; `curl` downloaded +the same assets using the environment trust store, without disabling TLS checks. +Restore then consumed the local archives. This is independent of the `gh` API +requests that returned `Forbidden`; the connected GitHub search returned no open +PRs at the initial check. + +## Reproduced checks + +All 12 real JSON documents in each snapshot pass their emitted schemas with +`jsonschema 4.26.0`. The validator previously returned success for an entirely +empty directory, printing 12 `skip` lines. It now fails missing documents, +missing schemas, malformed JSON, invalid schemas, and unresolved external +references. Eight offline regression tests cover those cases and valid input. + +Both snapshots reproduce all 27 checked artifacts with the same release binary +and `update --bundles ... --wa-version ... --check`. The old run took 13.970 s +with peak RSS 2,387,992 KiB; the new run took 13.114 s with peak RSS 2,386,528 KiB. +These are single warm runs measured with Python monotonic time and child-process +resource usage, not benchmarks. They prove reproduction, not protocol completeness. + +The lint result still fails six exact baselines. No limit was relaxed. Its old +`IMPROVED ... lower the baseline` diagnosis was unsound without source review; +it now reports a changed state and requests investigation. + +| Counted gap | Old baseline | New observed | Attribution | +|---|---:|---:|---| +| IQ attributes without argument paths | 55 | 49 | Privacy recovers 3; KMP catalog departure removes 3 | +| IQ children without argument paths | 12 | 10 | Privacy recovers 1; KMP catalog departure removes 1 | +| MEX undetermined variable presence | 108 | 106 | Removed operations subtract 6; ContactManager adds 3; SmartComposer adds 1 | +| MEX operations with no established presence | 13 | 10 | Four removed operations; one new SmartComposer operation | +| WAM uncataloged-event constructions | 41 | 29 | Indeterminate without per-construction attribution | +| WAM unread construction arguments | 104 | 93 | Identifier arguments 100→89; call 1 and member 3 unchanged; cause indeterminate | + +The IQ missing-content count stays at 23 through cancellation: KMP departure +subtracts one while SetAbout's changed selected entry point adds one. An unchanged +total therefore also needs identity-level review. + +## Source findings + +The compact [source sidecar](snapshot-quality-2026-10-07.sources.json) records +recoverable bundle hashes and AST byte spans for the findings below. All source +inspection used those AST ranges; no `eval`, `vm`, or authenticated client ran. + +`WAWebSetPrivacyJob` changes helper `f` from four positional parameters to an +object with `dhash`, `name`, `users`, and `value`. Both versions still construct +`privacy/category` and repeated `user` nodes. The new IR recovers the three +category argument paths and the child's `users[]` path. It also corrects the old +`name/action` and `name/wid` paths to `users[]/action` and `users[]/wid`. +This is recovered extraction quality enabled by an upstream refactor, not a +wire feature removal or proof that all branches are modeled. The old source is +bundle `fd26924373be0ce0e773c304511df62909fa76e11d9294c0509542d3f38f842d`, +bytes 1087286..1090426; the new source is +`7c7be0378ce2e7ff49b9f5909cf8a7d283000af82cc43bc071c2df3541ec5d56`, +bytes 183590..186813. + +`WAWebKmpSyncdRequestBuilder` contributed three unaddressed attributes, one child, +and one content value. It is present in the old AST index and not recovered in +the new one. This explains the counter arithmetic but does not establish removal +of app-state protocol support. The source-index API does not report parse errors, +and a renamed or relocated implementation needs separate investigation. Keep +this catalog departure indeterminate. + +`WAWebSetAboutJob` is not a proven rename from `setAbout` to `sendSetAbout`. +The new source exports both. It moves the IQ construction into a positional +`sendSetAbout` function, called from the persisted job. The selected IR export +changes, losing the old `content` argument-object path while retaining dynamic +status content. Treat the add/remove pair separately; consumers must not assume +that matching wire shapes establish function identity. + +Nine MEX operation names depart and three arrive. The departures are +CreateLabyrinthBackup, DebugLabyrinthInboxSnapshot, DebugLabyrinthRange, +EBMessageMetadataQuery, RotateLabyrinthEpoch, TeamLinkCreateInvitation, +TeamLinkListInvitations, TeamLinkRemoveInvitation, and UploadLabyrinthMessages. +The additions are AccountLinkingAPIGetCerts, UpdateNewsletterAdminProfileSetting, +and useWAWebSmartComposerCoachSuggestedReply. Four departures explain six fewer +undetermined keys and four fewer wholly undetermined operations. The new +SmartComposer operation adds one of each. + +ContactManagerCustomerProfiles adds three undetermined presence nodes under +`filters`, accounting for the remainder of the MEX counter change. Its source +now constructs a filters array with `.map`. The operation's persisted ID change +is independently proved by the literal exports in its facebookRelayOperation +module: `27747880408206174` becomes `27796221486653417`. Other changes, including +candidate_lids' shape becoming a string and presence becoming conditional, +remain unclassified pending a focused MEX inference review. An aggregate drop +must not hide those new gaps. + +WAM changes from 883 to 874 constructions, 807 to 810 call sites, and 121 to 126 +partial sites. These aggregates do not identify the 12 uncataloged-event and +11 unread-argument departures. Neither the lower gap counts nor the higher +call-site count justifies lowering the baselines before per-site attribution. + +## Review artifact and remaining work + +The generic report contains 472 deltas for this pair, including one opaque +protobuf artifact delta. The checked-in [review ledger](snapshot-quality-2026-10-07.reviews.json) +classifies the privacy improvement and the ContactManager persisted-ID change. +The remaining 470 deltas stay indeterminate; unreviewed is not equivalent to bad. +Generate the report with: + +```sh +whatspec diff old-generated generated --json \ + --evidence docs/snapshot-quality-2026-10-07.reviews.json > contracts.json +``` + +The raw report is 2,501,713 bytes. Full optional source indexes are 14,248,640 and +14,223,916 bytes, with 20,209 and 19,713 recovered module names. Only the focused +sidecar and review ledger are committed. The generated IR is unchanged. + +Before the snapshot can pass lint, account for the KMP departure and WAM sites, +and review the new MEX gaps. Coordinate extractor or IQ IR corrections with +those owners. Shared workflows are unchanged; the conformity work can integrate +`python3 -m unittest discover -s scripts/tests -v`. CI must be checked on the +published SHA, and a failing baseline must remain visible until resolved. diff --git a/docs/snapshot-quality-2026-10-07.reviews.json b/docs/snapshot-quality-2026-10-07.reviews.json new file mode 100644 index 00000000..9f5914ea --- /dev/null +++ b/docs/snapshot-quality-2026-10-07.reviews.json @@ -0,0 +1,23 @@ +{ + "reportVersion": 1, + "reviews": [ + { + "id": "d8a128b58ba26a3247e9218bd8403bb47a8eb365e938ada5be9411e4b8179d8a", + "classification": "extraction-improvement", + "basis": "The upstream helper changes from four positional arguments to one object. Its category attributes and repeated user child remain present. The new IR recovers name/value/dhash and users[] paths; the old user paths incorrectly passed through name. This does not establish full branch coverage.", + "references": [ + "docs/snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebSetPrivacyJob/0", + "docs/snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebSetPrivacyJob/0" + ] + }, + { + "id": "98a7106103385fb386e200e5319bb7432a7f1fc61d86e9a74a9cfc8d5d324613", + "classification": "upstream-change", + "basis": "The preserved facebookRelayOperation module exports literal 27747880408206174 in the old set and 27796221486653417 in the new set. Both values agree with their IR. This establishes a persisted operation ID change in these snapshots, not server compatibility.", + "references": [ + "docs/snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation/0", + "docs/snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation/0" + ] + } + ] +} diff --git a/docs/snapshot-quality-2026-10-07.sources.json b/docs/snapshot-quality-2026-10-07.sources.json new file mode 100644 index 00000000..6f8a2e72 --- /dev/null +++ b/docs/snapshot-quality-2026-10-07.sources.json @@ -0,0 +1,198 @@ +{ + "old": { + "bundleCount": 516, + "coverage": "recovered-definitions-only", + "limits": [ + "Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", + "The existing AST extractor does not expose parse diagnostics. Absence from this index is not proof of absence upstream.", + "All recovered occurrences are retained. Equal module names with different hashes need review.", + "Module provenance locates code; field-level semantics may require following dependencies." + ], + "modules": { + "WAWebContactManagerCustomerProfilesQuery": [ + { + "bundleSha256": "792466f7a00cd5862ac782f916733975cd9f46800bb9fd9679a1301411c24508", + "dependencies": [ + "WAJids", + "WALogger", + "WAWebContactManagerCustomerProfileDecoders", + "WAWebContactManagerCustomerProfilesQuery.graphql", + "WAWebFetchAdAccountToken", + "WAWebNetworkStatus", + "WAWebRelayClient", + "asyncToGeneratorRuntime", + "err" + ], + "end": 82111, + "factorySha256": "3171938da8ccd6a15512edc6048de4af70377b8323e4a6d6a722280d5bcbe643", + "moduleSha256": "24015b54d3434667ed9505c5dda69620c55d4f0d26c09d10b6f1f78243a70553", + "start": 80121 + } + ], + "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation": [ + { + "bundleSha256": "792466f7a00cd5862ac782f916733975cd9f46800bb9fd9679a1301411c24508", + "dependencies": [], + "end": 78398, + "factorySha256": "408708e358bf032817a78162c7c38596128adcb4e3e9d2ad24609f703817fea1", + "moduleSha256": "011cdf275eef4fd3874829777992f69bf9172c712d8e6d40f609186bab73868f", + "start": 78265 + } + ], + "WAWebKmpSyncdRequestBuilder": [ + { + "bundleSha256": "ffe69f61924eb23c1d65cb89a152e4ce35ee404bd52a7edcced9c037b236f490", + "dependencies": [ + "WALogger", + "WAWap", + "WAWebGetPendingMutation", + "WAWebKmpKotlinUtils", + "WAWebKmpSyncdCollectionNameUtils", + "WAWebKmpSyncdMutationUtils", + "WAWebKmpSyncdResultUtils", + "WAWebKmpSyncdServiceLocator", + "WAWebRunInTransaction", + "WAWebSyncdConst", + "WAWebSyncdRequestBuilderUtils", + "asyncToGeneratorRuntime" + ], + "end": 3410928, + "factorySha256": "7af76b2a50b2ee343bf971446ecea65c7fabdf3623b3ebe81effb580570db7cd", + "moduleSha256": "04a74de7f7e834f5d08bf98f07a6e1dd69b222f5493c34d9091230ca9861aa03", + "start": 3406431 + } + ], + "WAWebSetAboutJob": [ + { + "bundleSha256": "82f783508805f734f16747c4bd0670a496f13004e8a0acd11fe5f45efaffd754", + "dependencies": [ + "WADeprecatedSendIq", + "WADeprecatedWapParser", + "WAWap", + "WAWebDefinePersistedJob", + "asyncToGeneratorRuntime" + ], + "end": 2666232, + "factorySha256": "341f818ca9fb353431570fe800d15d947d9d46f1ebf5112ee71a1b667d469756", + "moduleSha256": "c6075a7a8ade03e66535299b3df337bb8ff78c3f67d377ca2b2237a3e50004c1", + "start": 2665475 + } + ], + "WAWebSetPrivacyJob": [ + { + "bundleSha256": "fd26924373be0ce0e773c304511df62909fa76e11d9294c0509542d3f38f842d", + "dependencies": [ + "$InternalEnum", + "WADeprecatedSendIq", + "WADeprecatedWapParser", + "WALogger", + "WAWap", + "WAWebABProps", + "WAWebBackendErrors", + "WAWebCommsWapMd", + "WAWebLidMigrationUtils", + "WAWebQueryPrivacyDisallowedListUtil", + "WAWebUsernameTypes", + "asyncToGeneratorRuntime", + "err" + ], + "end": 1090426, + "factorySha256": "eda0360705bee2806116cdf41a63b6a6e4c754f921a1cbe4d3c66a9a5b6bb839", + "moduleSha256": "01098f3de86fbfcf023a317a096dae1c05b0794b92b4488bde1421dd10f0c72e", + "start": 1087286 + } + ] + }, + "setHash": "99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00", + "sourceIndexVersion": 1, + "waVersion": "2.3000.1045368834" + }, + "new": { + "bundleCount": 579, + "coverage": "recovered-definitions-only", + "limits": [ + "Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", + "The existing AST extractor does not expose parse diagnostics. Absence from this index is not proof of absence upstream.", + "All recovered occurrences are retained. Equal module names with different hashes need review.", + "Module provenance locates code; field-level semantics may require following dependencies." + ], + "modules": { + "WAWebContactManagerCustomerProfilesQuery": [ + { + "bundleSha256": "36dc1290c51b02500ad640ba5aa793dd1b5e9172afcb71ff327d64cd1f279717", + "dependencies": [ + "WAJids", + "WALogger", + "WAWebContactManagerCustomerProfileDecoders", + "WAWebContactManagerCustomerProfilesQuery.graphql", + "WAWebFBLogger", + "WAWebFetchAdAccountToken", + "WAWebGraphQLServerError", + "WAWebNetworkStatus", + "WAWebRelayClient", + "asyncToGeneratorRuntime", + "err", + "getErrorSafe" + ], + "end": 257662, + "factorySha256": "eb5e7cf190d44a5cbf268623c952ee43e04e348a88b627b196d37d83a31dab66", + "moduleSha256": "eea21df5ba17114fbce760a1d44502f48d0a7d4ba66a4dfbad4e6ce00f8f92ca", + "start": 254574 + } + ], + "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation": [ + { + "bundleSha256": "36dc1290c51b02500ad640ba5aa793dd1b5e9172afcb71ff327d64cd1f279717", + "dependencies": [], + "end": 252770, + "factorySha256": "8ad8f73b1a267fb2b3e43ac1c801326dd5a667f104e3a45fb33b81b01bc99b4c", + "moduleSha256": "0edd9a4622782763c39f80764decf75406cbec76925a80622cfd1ee90436ec81", + "start": 252637 + } + ], + "WAWebSetAboutJob": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WADeprecatedSendIq", + "WADeprecatedWapParser", + "WAWap", + "WAWebDefinePersistedJob", + "asyncToGeneratorRuntime" + ], + "end": 2111895, + "factorySha256": "5fe82a38fe4610426641d5c0351746693a33b095254e8a358c1b1a3df7a326ef", + "moduleSha256": "6b2d764d05842a76912d40ce70154f6ec5a69f67de25345a2d86d8cfa8c32db0", + "start": 2111070 + } + ], + "WAWebSetPrivacyJob": [ + { + "bundleSha256": "7c7be0378ce2e7ff49b9f5909cf8a7d283000af82cc43bc071c2df3541ec5d56", + "dependencies": [ + "$InternalEnum", + "WADeprecatedSendIq", + "WADeprecatedWapParser", + "WALogger", + "WAWap", + "WAWebABProps", + "WAWebBackendErrors", + "WAWebCommsWapMd", + "WAWebLidMigrationUtils", + "WAWebQueryPrivacyDisallowedListUtil", + "WAWebUsernameTypes", + "asyncToGeneratorRuntime", + "err" + ], + "end": 186813, + "factorySha256": "f2e16b840638226fe6023fe91bb0bbf3bcae8b5bc31967504cba8db759748abb", + "moduleSha256": "270ab2fc00118aae4e79742cd84606290ed14b06d80a5c83da46cdbe2573cb60", + "start": 183590 + } + ] + }, + "setHash": "05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b", + "sourceIndexVersion": 1, + "waVersion": "2.3000.1047483476" + } +} diff --git a/docs/snapshot-quality.md b/docs/snapshot-quality.md new file mode 100644 index 00000000..5d5270c4 --- /dev/null +++ b/docs/snapshot-quality.md @@ -0,0 +1,108 @@ +# Snapshot quality review + +The existing text `whatspec diff OLD NEW` is a count and name summary. Use +`whatspec diff OLD NEW --json` for contract deltas across every manifest domain. +The report checks domain hashes, document versions, and lock self-consistency. +It fails on missing declared artifacts. Run schema validation separately, because +matching a manifest hash does not establish schema conformance. + +```sh +python3 scripts/validate-schemas.py generated +python3 scripts/lint-ir.py generated +python3 -m unittest discover -s scripts/tests -v +cargo run --release -p whatspec -- diff old-generated generated --json > contracts.json +``` + +The JSON report has its own `reportVersion: 1`. It is an additive CLI output, +not a change to IR `schemaVersion`. No runtime consumer policy changes. In +particular, strict maintenance checks do not require clients to reject unknown +extensions in received traffic. + +Each delta includes before/after values, document SHA-256, JSON Pointers, bundle +set hashes, and a stable ID bound to those values. Enums use `(module, name)`. +MEX and app-state use the document's map keys. Other recognized top-level +collections use explicit keys listed in `contract_diff.rs`. Duplicate keys stay +in an ordered group with original indices; they are never silently deduplicated. +Nested arrays retain order and multiplicity, including response alternatives. +Their full before/after arrays are emitted rather than inventing field identities. +An unkeyed collection can therefore produce a larger delta on reordering. +Protobuf changes are opaque artifact hash deltas, not parsed field diffs. + +No shape-based rename detection occurs. An export or module name change remains +an addition and removal unless a reviewer establishes the relationship. The tool +does not turn a renamed export into a guaranteed operation identity or promise +API compatibility. Schema version differences are explicit report metadata. + +## Recovering source evidence + +Restore each snapshot from its own `bundles.lock.json`, using the corresponding +`bundle-store` asset. The restore command checks bundle hashes, sizes, set hash, +and multiplicity. Archive digests and bundle set hashes are different values. + +```sh +whatspec restore --from-lock generated/bundles.lock.json --out bundles +whatspec source-index generated/bundles.lock.json bundles WAWebSetPrivacyJob > sources.json +``` + +`source-index` independently checks the exact bundle multiset. It parses modules +with the existing AST extractor and emits a separate optional sidecar containing +bundle SHA-256, module and factory hashes, dependency names, and byte offsets. +It never evaluates JavaScript. Source names may be selected after the two paths; +omit them to index all recovered modules. Offsets are UTF-8 bytes with an +exclusive end, so invalid UTF-8 is rejected rather than decoded lossily. +Every recovered occurrence remains visible, including conflicting definitions. +The index is deterministic and contains no workspace-specific file paths. + +The existing AST API does not expose parse diagnostics. Consequently the sidecar +states `coverage: recovered-definitions-only`: a missing name is not proof of +upstream removal. A module hint in a contract report is also only a starting +point. Follow dependencies and mixins before attributing a field's semantics. +For a field-level review, cite both its IR pointer and the relevant module span. +This keeps recoverable provenance outside every repeated field of the IR. + +## Classifying a change + +Every delta starts as `indeterminate`. A smaller count, a different source hash, +or an unchanged generator version string is insufficient to classify it. + +- `upstream-change` needs a source change that explains the contract delta, such + as the old and new persisted operation ID literals. +- `extraction-improvement` needs evidence that the source supports the newly + recovered constraint. A source refactor can make the same extractor recover + more information. This is different from proving an extractor code fix. +- `extraction-loss` needs a source constraint that survives but is missing or + distorted in the new IR. Reproduce it with the pinned inputs before fixing it. +- `indeterminate` records unresolved causes, absent modules without complete + coverage, and aggregate diagnostic changes without per-site attribution. + +Store reviewed assessments in a small JSON file: + +```json +{ + "reportVersion": 1, + "reviews": [{ + "id": "exact ID from contracts.json", + "classification": "upstream-change", + "basis": "Describe the source observation and its limits.", + "references": ["sources.json#/modules/ExactModule/0"] + }] +} +``` + +Apply it with `whatspec diff OLD NEW --json --evidence reviews.json`. Stale IDs, +duplicate IDs, unknown classes, and empty evidence fail. Any edit to a referenced +artifact invalidates its reviews, even if that edit is in another operation. This +conservative rule costs a review refresh after regeneration. These are explicitly +reviewed assessments, not machine-verified proofs: the CLI does not fetch or +interpret reference strings. Code review must validate their contents. Classify +mixed changes as indeterminate or explain each component in the basis. + +For extraction fixes, regenerate before and after with the same verified bundle +set. For upstream updates, keep generator code fixed across both input sets, then +inspect changed source paths. Preserve unresolved gaps and exact lint baselines +until the review explains them. Do not normalize a loss into a lower limit. + +The tools add no dependencies, committed full source index, or per-field IR +metadata. Reports are generated on demand. Full ordered arrays can be sizeable; +keep only focused evidence and assessments in version control. See the dated +investigation for measured cost and remaining baseline blockers. diff --git a/scripts/lint-ir.py b/scripts/lint-ir.py index a57c9f39..34e52752 100755 --- a/scripts/lint-ir.py +++ b/scripts/lint-ir.py @@ -15,8 +15,8 @@ two things at once. These always fail. * **counted** — a known, legitimately non-empty state, held to a BASELINE. An enum WA composes at runtime cannot be resolved, and the IR records that under - `dropsByReason` rather than pretending; the count may shrink (extraction - improved) but a rise means a new construct started slipping through unnoticed. + `dropsByReason` rather than pretending. Counts can change because of upstream + edits, extraction changes, or missing inputs. Neither direction establishes a cause without source evidence. Network-free and deterministic, so it runs in CI beside the schema validation. @@ -2074,9 +2074,9 @@ def visit(node, path, domain=domain): continue ok = False if was == 0: - print(f"REGRESSION newly flattened shape: {ident} (x{now})") + print(f"CHANGED newly flattened shape: {ident} (x{now})") elif now == 0: - print(f"IMPROVED shape no longer flattened: {ident} — drop it") + print(f"CHANGED flattened shape absent: {ident} — investigate source and extraction") else: print(f"CHANGED {ident}: {was} -> {now}") @@ -2086,9 +2086,9 @@ def visit(node, path, domain=domain): continue ok = False if was == 0: - print(f"REGRESSION newly unresolved enum: {ident} (x{now})") + print(f"CHANGED newly unresolved enum: {ident} (x{now})") elif now == 0: - print(f"IMPROVED enum now resolved: {ident} — drop it from the baseline") + print(f"CHANGED unresolved enum absent: {ident} — investigate source and extraction") else: print(f"CHANGED {ident}: {was} -> {now} — update the baseline") if ok: @@ -2100,14 +2100,14 @@ def visit(node, path, domain=domain): for name, observed in sorted(counts.items()): allowed = BASELINE[name] if observed > allowed: - print(f"REGRESSION {name}: {observed} (baseline {allowed})") + print(f"CHANGED {name}: {observed} (baseline {allowed})") ok = False elif observed < allowed: # A RATCHET, not an upper bound. Accepting a decrease silently banks the # difference as slack: 157 -> 150 followed by seven newly unresolved enums is # back at 157 and passes, which is exactly the drift this is supposed to catch. - # An improvement is real work and updating the number with it costs one line. - print(f"IMPROVED {name}: {observed} (baseline {allowed}) — lower the baseline") + # A fall may also mean an operation disappeared; investigate before repinning. + print(f"CHANGED {name}: {observed} (baseline {allowed}) — investigate source and extraction") ok = False else: print(f"ok {name}: {observed} (baseline {allowed})") @@ -2120,8 +2120,8 @@ def visit(node, path, domain=domain): return 1 if not ok: print( - "\na counted state left its baseline — raise means a constraint is being lost, " - "fall means the baseline owes an update" + "\na counted state left its baseline; compare contracts and pinned sources " + "before changing it. A falling count does not prove improved extraction." ) return 1 print(f"\n{len(docs)} document(s) internally consistent") diff --git a/scripts/tests/test_snapshot_validation.py b/scripts/tests/test_snapshot_validation.py new file mode 100644 index 00000000..49e548b6 --- /dev/null +++ b/scripts/tests/test_snapshot_validation.py @@ -0,0 +1,65 @@ +"""Offline regressions for incomplete and malformed generated snapshots.""" +import contextlib +import importlib.util +import io +import json +from pathlib import Path +import tempfile +import unittest + +SCRIPT = Path(__file__).resolve().parents[1] / 'validate-schemas.py' +spec = importlib.util.spec_from_file_location('validate_schemas', SCRIPT) +validator = importlib.util.module_from_spec(spec) +spec.loader.exec_module(validator) + + +class SnapshotValidation(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) + for document, schema in validator.DOMAINS: + for rel, value in [(document, {}), (schema, {'type': 'object'})]: + path = self.root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value)) + + def validate(self): + with contextlib.redirect_stdout(io.StringIO()): + return validator.validate(self.root) + + def test_complete_snapshot(self): + self.assertEqual(self.validate(), 0) + + def test_missing_document_is_failure(self): + (self.root / validator.DOMAINS[0][0]).unlink() + self.assertEqual(self.validate(), 1) + + def test_missing_schema_is_failure(self): + (self.root / validator.DOMAINS[0][1]).unlink() + self.assertEqual(self.validate(), 1) + + def test_empty_snapshot_is_failure(self): + with tempfile.TemporaryDirectory() as empty: + with contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(validator.validate(Path(empty)), 1) + + def test_invalid_schema_is_failure(self): + (self.root / validator.DOMAINS[0][1]).write_text('{"type":"not-a-type"}') + self.assertEqual(self.validate(), 1) + + def test_malformed_document_is_failure(self): + (self.root / validator.DOMAINS[0][0]).write_text('{') + self.assertEqual(self.validate(), 1) + + def test_wrong_shape_is_failure(self): + (self.root / validator.DOMAINS[0][0]).write_text('[]') + self.assertEqual(self.validate(), 1) + + def test_external_reference_is_failure_without_network(self): + (self.root / validator.DOMAINS[0][1]).write_text('{"$ref":"https://invalid.example/schema.json"}') + self.assertEqual(self.validate(), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/validate-schemas.py b/scripts/validate-schemas.py index c63c4ff1..5c156bbc 100755 --- a/scripts/validate-schemas.py +++ b/scripts/validate-schemas.py @@ -12,6 +12,9 @@ try: from jsonschema import Draft202012Validator + from jsonschema.exceptions import SchemaError + from referencing import Registry + from referencing.exceptions import Unresolvable except ImportError: sys.exit("jsonschema not installed — `pip install jsonschema`") @@ -32,18 +35,26 @@ ] -def main() -> int: - root = Path(sys.argv[1] if len(sys.argv) > 1 else "generated") +def validate(root: Path) -> int: failures = 0 for doc_rel, schema_rel in DOMAINS: doc_path, schema_path = root / doc_rel, root / schema_rel if not doc_path.exists() or not schema_path.exists(): - print(f"skip {doc_rel}: missing document or schema") + failures += 1 + print(f"FAIL {doc_rel}: missing document or schema") + continue + try: + schema = json.loads(schema_path.read_text()) + document = json.loads(doc_path.read_text()) + Draft202012Validator.check_schema(schema) + # No remote schema retrieval: emitted schemas must be self-contained. + validator = Draft202012Validator(schema, registry=Registry()) + errors = sorted(validator.iter_errors(document), + key=lambda e: tuple(str(p) for p in e.path)) + except (OSError, ValueError, SchemaError, Unresolvable) as err: + failures += 1 + print(f"FAIL {doc_rel} (against {schema_rel}): {err}") continue - schema = json.loads(schema_path.read_text()) - document = json.loads(doc_path.read_text()) - validator = Draft202012Validator(schema) - errors = sorted(validator.iter_errors(document), key=lambda e: e.path) if errors: failures += 1 print(f"FAIL {doc_rel} (against {schema_rel}):") @@ -58,4 +69,4 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + sys.exit(validate(Path(sys.argv[1] if len(sys.argv) > 1 else "generated"))) From 0c692688a48b2644d02ff49fd88c2de90224390e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:45:19 -0300 Subject: [PATCH 03/52] fix(codegen): reject degraded IQ response parsers explicitly --- Cargo.lock | 2 +- crates/wa-codegen/Cargo.toml | 2 +- crates/wa-codegen/src/lib.rs | 5 +- crates/wa-codegen/src/spec.rs | 319 +++++++++++++++++++++++++--------- docs/iq-response-admission.md | 149 ++++++++++++++++ 5 files changed, 388 insertions(+), 89 deletions(-) create mode 100644 docs/iq-response-admission.md diff --git a/Cargo.lock b/Cargo.lock index bb27cc68..7a7ead95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2151,7 +2151,7 @@ dependencies = [ [[package]] name = "wa-codegen" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "regex", diff --git a/crates/wa-codegen/Cargo.toml b/crates/wa-codegen/Cargo.toml index 6f44cf89..6f489bae 100644 --- a/crates/wa-codegen/Cargo.toml +++ b/crates/wa-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wa-codegen" -version = "0.1.0" +version = "0.2.0" edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/wa-codegen/src/lib.rs b/crates/wa-codegen/src/lib.rs index 5cf1ca37..1c32edb5 100644 --- a/crates/wa-codegen/src/lib.rs +++ b/crates/wa-codegen/src/lib.rs @@ -826,7 +826,7 @@ mod tests { } #[test] - fn confirmation_spec_has_unit_response() { + fn unrecovered_response_keeps_legacy_type_but_returns_error() { let ir = IqIr { wa_version: "0.0.0".into(), stanzas: vec![IqStanzaDef { @@ -857,7 +857,8 @@ mod tests { assert!(c.contains("pub struct AckSpec;")); // no fields → unit struct assert!(c.contains("type Response = ();")); assert!(c.contains("InfoQuery::set(W_X_NAMESPACE, Jid::new(\"\", Server::Group), None)")); - assert!(c.contains("Ok(())")); + assert!(!c.contains("Ok(())")); + assert!(c.contains("response.contract_missing")); } #[test] diff --git a/crates/wa-codegen/src/spec.rs b/crates/wa-codegen/src/spec.rs index 407ba8f4..1d014cf6 100644 --- a/crates/wa-codegen/src/spec.rs +++ b/crates/wa-codegen/src/spec.rs @@ -177,9 +177,9 @@ fn fail_required_fields(fields: &[wa_ir::ParsedField]) -> std::collections::BTre /// Emit, for an RPC outcome-union response, a `#[derive(Default)]` struct per /// variant (and its child item structs) plus an `enum` wrapping them. Returns /// `(variant_name, struct_name)` per variant for the parser — but only when EVERY -/// variant's parser validates ([`parser_is_valid`]); otherwise `None` (and nothing -/// emitted), so the caller falls back to the single-shape/`()` path rather than -/// generating an invalid parser. Models the IR's outcome wire shape, not domain types. +/// variant's parser validates ([`parser_is_valid`]); otherwise an error identifies +/// the variant and nothing is emitted. The caller retains legacy response types +/// but emits a diagnostic error instead of silently dropping outcomes. /// A value a response variant pins before it will accept a node. /// /// Two kinds, because an outcome root has two things to pin: an ATTRIBUTE by name, and the @@ -212,7 +212,7 @@ fn variant_pins(v: &ResponseVariant) -> Vec> { /// The same pins as generated conditions. fn pin_conditions(v: &ResponseVariant, node: &str) -> Vec { - variant_pins(v) + let mut conditions: Vec = variant_pins(v) .into_iter() .map(|pin| match pin { Pin::Attr(name, value) => format!( @@ -225,7 +225,16 @@ fn pin_conditions(v: &ResponseVariant, node: &str) -> Vec { rust_lit(value), ), }) - .collect() + .collect(); + // flattenedChildWithTag is a presence guard even when no payload is read. + // Keep it in both the admission signature and the emitted selector. + conditions.extend(v.assertions.iter().filter_map(|a| { + (a.kind == AssertionKind::Child) + .then_some(a.name.as_deref()) + .flatten() + .map(|name| format!("{node}.get_optional_child({}).is_some()", rust_lit(name))) + })); + conditions } /// Whether a node satisfying `mine` could satisfy `other` as well. @@ -254,7 +263,7 @@ fn emit_outcome_types( enum_name: &str, doc: &str, out: &mut Vec, -) -> Option> { +) -> Result, String> { let tags: Vec = op.response.variants.iter().map(|v| v.tag.clone()).collect(); let plen = variant_tag_prefix(&tags); @@ -279,14 +288,13 @@ fn emit_outcome_types( } names.push((vname.clone(), format!("{spec_base}{vname}"))); } - let all_valid = op - .response - .variants - .iter() - .zip(&names) - .all(|(v, (_, sname))| parser_is_valid(&v.fields, sname, sname)); - if !all_valid { - return None; + for (index, (variant, (_, name))) in op.response.variants.iter().zip(&names).enumerate() { + if !parser_is_valid(&variant.fields, name, name) { + return Err(format!( + "outcomes.unemittable: response.variants[{index}] {} has an unemittable payload parser", + variant.tag + )); + } } // Bail if the try-each would still be ambiguous. A variant's parser accepts a @@ -302,7 +310,17 @@ fn emit_outcome_types( .response .variants .iter() - .map(|v| fail_required_fields(&v.fields)) + .map(|v| { + let mut required = fail_required_fields(&v.fields); + for assertion in &v.assertions { + if assertion.kind == AssertionKind::Child + && let Some(name) = &assertion.name + { + required.insert(format!("/child:{name}")); + } + } + required + }) .collect(); for i in 0..req.len() { for j in (i + 1)..req.len() { @@ -312,7 +330,10 @@ fn emit_outcome_types( if req[i].is_subset(&req[j]) && !assertions_conflict(&op.response.variants[i], &op.response.variants[j]) { - return None; + return Err(format!( + "outcomes.unemittable: response.variants[{i}] {} can shadow response.variants[{j}] {}", + op.response.variants[i].tag, op.response.variants[j].tag + )); } } } @@ -368,7 +389,7 @@ fn emit_outcome_types( } out.push("}".to_string()); out.push(String::new()); - Some(info) + Ok(info) } /// Whether `op` actually generates an RPC outcome-union `enum` (vs falling back to the @@ -381,7 +402,7 @@ pub(crate) fn op_uses_outcome_union(op: &IqStanzaDef, child_prefix: &str) -> boo } let enum_name = format!("{child_prefix}Response"); let mut sink = Vec::new(); - emit_outcome_types(op, child_prefix, &enum_name, "", &mut sink).is_some() + emit_outcome_types(op, child_prefix, &enum_name, "", &mut sink).is_ok() } /// Emit the `parse_response` body for an outcome union: try each variant in order @@ -682,23 +703,27 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - let child_prefix = spec_name.trim_end_matches("Spec"); // An RPC outcome union (`response.variants`) becomes an `enum` over per-variant // structs — the wire-shape outcomes (success/error) the IR records, which the - // single-struct path below can't express. Falls through to that path when any - // variant's parser can't be generated cleanly (`emit_outcome_types` → None). + // single-struct path below can't express. When any variant cannot be emitted, + // retain the old fallback type but refuse parsing with a generation diagnostic. let mut outcome_info: Vec<(String, String)> = Vec::new(); let mut use_union = false; + let mut outcome_error = None; let mut response_type_name: String = "()".to_string(); if !op.response.variants.is_empty() { let enum_name = format!("{child_prefix}Response"); - if let Some(info) = emit_outcome_types( + match emit_outcome_types( op, child_prefix, &enum_name, &format!("{doc_owner}:{iq}"), &mut lines, ) { - response_type_name = enum_name; - outcome_info = info; - use_union = true; + Ok(info) => { + response_type_name = enum_name; + outcome_info = info; + use_union = true; + } + Err(reason) => outcome_error = Some(reason), } } if !use_union { @@ -734,53 +759,12 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - // parse_response — validate the parser can produce all struct fields. Skipped // for outcome unions, which generate their own per-variant try-each parser. - let mut can_generate = !effectively_confirmation && !use_union; - if can_generate { - let (check_fields, check_child_structs, _) = - collect_response_fields(&op.response.fields, child_prefix); - let names: Vec<&str> = check_fields.iter().map(|f| f.name.as_str()).collect(); - if names.iter().collect::>().len() != names.len() { - can_generate = false; - } - if can_generate { - let parser_code = emit_response_parser( - &op.response.fields, - &response_type_name, - " ", - child_prefix, - ) - .join("\n"); - for f in &check_fields { - if !parser_code.contains(&format!("{},", f.name)) - && !parser_code.contains(&format!("{}:", f.name)) - { - can_generate = false; - break; - } - } - if can_generate && LET_KEYWORD.is_match(&parser_code) { - can_generate = false; - } - if can_generate { - 'outer: for cs in &check_child_structs { - let required: HashSet<&str> = - cs.fields.iter().map(|f| f.name.as_str()).collect(); - for body in struct_init_bodies(&parser_code, &cs.name) { - let inited: HashSet<&str> = INIT_FIELD - .captures_iter(body) - .map(|c| c.get(1).unwrap().as_str()) - .collect(); - if !required.iter().all(|r| inited.contains(r)) { - can_generate = false; - break 'outer; - } - } - } - } - } - } + let can_generate = !effectively_confirmation + && !use_union + && parser_is_valid(&op.response.fields, &response_type_name, child_prefix); - // Fall back to () if the parser can't be generated cleanly (non-union path). + // Retain the legacy associated type when no parser can be emitted. The parse + // method below returns an explicit generation error instead of confirming success. if !can_generate && !effectively_confirmation && !use_union { let marker = format!("/// Response from {doc_owner}:{iq}."); if let Some(start) = lines.iter().rposition(|l| *l == marker) { @@ -797,12 +781,34 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - } } - let resp_param = if effectively_confirmation || response_type_name == "()" { - "_response" + // A missing shape is not evidence of an empty successful result. Preserve the + // existing associated type for source compatibility, but make degradation an + // explicit error. An explicit empty outcome still uses its admitted union. + let rejection = if let Some(reason) = outcome_error.as_deref() { + Some(reason) + } else if !use_union && op.response.fields.is_empty() { + Some("response.contract_missing: no recovered response fields or outcomes") + } else if !use_union && effectively_confirmation { + Some("response.payload_unemittable: recovered fields produced no response payload") + } else if !use_union && !can_generate { + Some("response.parser_unemittable: parser cannot initialize the recovered payload") } else { - "response" + None }; - if use_union { + + let resp_param = + if rejection.is_some() || effectively_confirmation || response_type_name == "()" { + "_response" + } else { + "response" + }; + if let Some(reason) = rejection { + lines.push(" fn parse_response(&self, _response: &wacore_binary::NodeRef<'_>) -> Result {".to_string()); + lines.push(format!( + " anyhow::bail!({})", + rust_lit(&format!("{}: {reason}", op.module_name)), + )); + } else if use_union { lines.push( " #[allow(clippy::needless_update, unused_variables, clippy::redundant_closure_call)]" .to_string(), @@ -816,11 +822,6 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - &response_type_name, " ", )); - } else if effectively_confirmation || response_type_name == "()" { - lines.push(format!( - " fn parse_response(&self, {resp_param}: &wacore_binary::NodeRef<'_>) -> Result {{" - )); - lines.push(" Ok(())".to_string()); } else { lines.push(" #[allow(clippy::needless_update, unused_variables)]".to_string()); lines.push(format!( @@ -840,6 +841,17 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - lines.push(" }".to_string()); lines.push("}".to_string()); + if let Some(reason) = rejection { + lines.push(String::new()); + lines.push(format!("impl {spec_name} {{")); + lines.push(" /// Why this reference parser always returns an error. This is a codegen limitation, not a wire rejection policy.".to_string()); + lines.push(format!( + " pub const RESPONSE_GENERATION_ERROR: &'static str = {};", + rust_lit(reason) + )); + lines.push("}".to_string()); + } + fix_unused_vars(lines.join("\n")) } @@ -1458,10 +1470,9 @@ mod tests { ); } #[test] - fn fallback_parser_keeps_a_childless_success_reachable() { - // End to end on the AcceptGroupAdd shape: a gated success beside a bare - // one falls back to a single-shape parser, which mirrors the union and - // must not require the gated variant's child. + fn ordered_child_guard_keeps_both_success_outcomes_reachable() { + // The gated success must be tried before the bare success. Flattening the + // two loses the approval outcome even though both stanzas still parse. use wa_ir::ResponseAssertion; use wa_ir::{ParsedField, ParsedFieldType}; fn typ() -> ParsedField { @@ -1514,8 +1525,21 @@ mod tests { }; let code = generate_spec(&op, "W_G2_NAMESPACE", "MakeAcceptGroupAddRequestSpec"); assert!( - !code.contains("get_optional_child"), - "the flattened parser must accept the bare success: {code}" + code.contains("pub enum MakeAcceptGroupAddRequestResponse"), + "the two success outcomes must remain distinguishable: {code}" + ); + assert!( + code.contains("response.get_optional_child(\"membership_approval_request\").is_some()"), + "{code}" + ); + assert!( + code.find("::GroupJoinRequestSuccess(").unwrap() < code.find("::Success(").unwrap() + ); + op.response.variants.reverse(); + let reversed = generate_spec(&op, "W_G2_NAMESPACE", "MakeAcceptGroupAddRequestSpec"); + assert!( + reversed.contains("outcomes.unemittable"), + "a broad first arm shadows the gated one: {reversed}" ); } @@ -2081,6 +2105,131 @@ mod tests { ); } + #[test] + fn missing_response_contract_is_not_a_confirmation() { + let op = stanza("MissingResponse", None); + let code = generate_spec(&op, "TEST_NAMESPACE", "MissingResponseSpec"); + assert!( + !code.contains("Ok(())"), + "unrecovered response must fail: {code}" + ); + assert!(code.contains("response.contract_missing"), "{code}"); + } + + #[test] + fn discarded_response_payload_is_not_a_confirmation() { + let mut op = stanza("DiscardedPayload", None); + op.response.fields = vec![wa_ir::ParsedField { + name: "unresolvedPayload".into(), + ..Default::default() + }]; + let code = generate_spec(&op, "TEST_NAMESPACE", "DiscardedPayloadSpec"); + assert!( + !code.contains("Ok(())"), + "discarded payload must fail: {code}" + ); + assert!(code.contains("response.payload_unemittable"), "{code}"); + } + + #[test] + fn invalid_response_parser_is_not_a_confirmation() { + // Real nested optional/repeated shape whose emitted initializer is incomplete. + let ir: wa_ir::IqIr = + serde_json::from_str(include_str!("../../../generated/iq/index.json")).unwrap(); + let op = ir + .stanzas + .iter() + .find(|op| op.module_name == "WAWebQueryBusinessCategoriesJob") + .unwrap(); + assert!(!parser_is_valid( + &op.response.fields, + "CategoriesResponse", + "Categories" + )); + let code = generate_spec(op, "TEST_NAMESPACE", "CategoriesSpec"); + assert!(!code.contains("Ok(())"), "invalid parser must fail: {code}"); + assert!(code.contains("response.parser_unemittable"), "{code}"); + } + + #[test] + fn real_pilots_preserve_requests_but_do_not_claim_complete_outcomes() { + let ir: wa_ir::IqIr = + serde_json::from_str(include_str!("../../../generated/iq/index.json")).unwrap(); + for pilot in ["SetSubject", "AcceptGroupAdd"] { + let module = format!("WASmaxOutGroups{pilot}Request"); + let op = ir + .stanzas + .iter() + .find(|op| op.module_name == module) + .unwrap(); + assert_eq!(op.request.target, IqTarget::GroupJid); + assert_eq!(op.request.target_arg_path.as_ref().unwrap()[0].key, "iqTo"); + for variant in &op.response.variants { + for (wire, request) in [("id", "id"), ("from", "to")] { + assert!( + variant + .assertions + .iter() + .any(|a| a.kind == AssertionKind::Reference + && a.name.as_deref() == Some(wire) + && a.reference_path.as_deref() == Some(&[request.to_string()])) + ); + } + } + let code = generate_spec(op, "W_G2_NAMESPACE", &format!("{pilot}Spec")); + assert!(code.contains("self.target.clone()")); + assert!(code.contains("outcomes.unemittable")); + assert!(code.contains(&format!("{pilot}ResponseClientError can shadow"))); + assert!(code.contains(&format!("{pilot}ResponseServerError"))); + if pilot == "SetSubject" { + assert_eq!( + op.request.children[0] + .content + .as_ref() + .unwrap() + .arg_path + .as_ref() + .unwrap()[0] + .key, + "subjectElementValue" + ); + assert!(code.contains("subject_node.bytes(self.subject_content.clone())")); + } else { + assert!( + op.response.variants[0] + .tag + .ends_with("GroupJoinRequestSuccess") + ); + assert!(op.response.variants[1].tag.ends_with("ResponseSuccess")); + assert!( + op.response.variants[0] + .assertions + .iter() + .any(|a| a.kind == AssertionKind::Child + && a.name.as_deref() == Some("membership_approval_request")) + ); + for wire in ["code", "expiration", "admin"] { + assert!(code.contains(&format!("accept_node.attr(\"{wire}\""))); + } + } + } + } + + #[test] + fn explicit_empty_outcome_remains_a_success() { + let mut op = stanza("EmptyOutcome", None); + op.response.variants = vec![success_variant( + "EmptySuccess", + vec![conflict_attr("type", Some("result"))], + )]; + op.response.variants[0].fields.clear(); + let code = generate_spec(&op, "TEST_NAMESPACE", "EmptyOutcomeSpec"); + assert!(code.contains("pub enum EmptyOutcomeResponse"), "{code}"); + assert!(code.contains("return Ok(EmptyOutcomeResponse::"), "{code}"); + assert!(code.contains("Some(\"result\")"), "{code}"); + assert!(!code.contains("response.contract_missing"), "{code}"); + } + #[test] fn fix_unused_vars_underscores_single_use_bindings() { // `x` is used; `y`/`z` are not. diff --git a/docs/iq-response-admission.md b/docs/iq-response-admission.md new file mode 100644 index 00000000..afc475d8 --- /dev/null +++ b/docs/iq-response-admission.md @@ -0,0 +1,149 @@ +# IQ response generation admission + +This is the first response-emitter change, not a claim of complete IQ generation. +`wa-codegen` 0.2.0 stops reporting success when response generation loses a +contract, payload, or outcome. It does not change the language-neutral IQ schema +or add a runtime dependency. The schema remains 4.3.0. + +## Behavior and migration + +An operation whose response cannot be represented retains its previous fallback +associated response type, including `()` where that was the old type. Its parser +now always returns an error naming the operation and the generation limitation. +The generated spec also exposes `RESPONSE_GENERATION_ERROR: &'static str` when +its response generation is rejected. This constant is an emitter diagnostic, +not a wire protocol field. An operation without this constant is **not** thereby +certified complete. + +| Diagnostic | Meaning | +| --- | --- | +| `response.contract_missing` | No recovered response fields or explicit outcomes. Empty metadata does not prove an empty successful response. | +| `response.payload_unemittable` | Recovered fields produced no response payload. | +| `response.parser_unemittable` | The generated initializer cannot represent the recovered payload. | +| `outcomes.unemittable` | A variant parser cannot be emitted, or an earlier variant can shadow a later one under the emitter's known predicates. The message names variant indexes and tags. | + +An explicit, admitted empty outcome still produces a successful empty outcome +value after its selector matches. Rejected outcome unions no longer silently +use only their primary success mirror. Child-presence assertions participate in +both the admission signature and emitted selector. A gated success followed by +a bare success retains both branches; reversing them still fails admission. + +Consumers upgrading from 0.1.0 must review the diagnostics for their selected +operations. Use a reviewed implementation for a rejected operation until its +missing semantics are supported. Do not convert the new errors back into unit +success. Request constructors and existing fallback response types remain; +newly admitted unions can change an associated response from a struct to an enum. +This behavior change is why the emitter version changes to 0.2.0. No release is +published by this change. + +This is generation-time rigor, not a policy requiring runtime rejection of every +unknown extension in an otherwise supported stanza. + +## Admission design for coordination + +A future strict entry point needs a report keyed by `moduleName` and wire path, +with independent status and diagnostics for each dimension below. Status must +separate supported, unsupported, and not established; an absent diagnostic cannot +mean that unknown semantics were proved absent. Generation is admitted only when +all dimensions required by the selected operation are supported. This report is +a proposed interface, **not implemented by the response-only constant above**. + +| Dimension | Required evidence / rule | Status in this patch | +| --- | --- | --- | +| Request | Known target, argument mapping, content, child cardinality and variants; no invented inputs. | Existing request emitter preserved; pilot mappings tested. No general strict request admission. | +| Response | All recovered payload fields emitted; explicit empty outcome distinguished from missing extraction. | Missing/fully discarded payload and invalid initializer fail explicitly. This is not a recursive completeness proof. | +| Outcomes | Source order retained and every supported outcome reachable. No primary-success fallback. | Rejected unions fail explicitly. Child presence is handled. Error-arm discrimination remains a pilot blocker. | +| Constraints | Literal values, ranges, enum policy, error code/text pairings and reference constraints retained. | No new global constraint-admission claim. Existing `pending_drops` is not serialized, so it cannot prove per-operation recovery from saved IR. | +| Guards | Only demonstrated guards necessary for the selected operation. Request correlation must receive the actual request context. | Child-presence guard added. Correlation is not implemented in this patch; no universal expression AST is introduced. | + +Proposed correlation input is the actual request's `id` and `to`, independent of +any caller's transport, event model or application API. The legacy response +method has no generated request ID. A follow-up must define a context-taking +entry point before claiming checks of `response.id == request.id` and +`response.from == request.to`. Do not infer these from an unrelated request or +hardcode the server address. Any IR/schema additions for recovery status need +coordination with the snapshot and conformance fronts; none are made here. + +## Pilot findings + +Both pilots remain rejected by full outcome admission in this patch. The gate +cannot distinguish their client-error and server-error arms using its current +signature, although the IR carries their error vocabularies. Admitting only the +success variants would conceal that limitation. + +| Operation | Request evidence | Ordered outcomes | Current emitter diagnostic | +| --- | --- | --- | --- | +| SetSubject | `GROUP_JID(iqTo)`; `` content is `subjectElementValue`. | Success, client error, server error. | `response.variants[1] SetSubjectResponseClientError can shadow response.variants[2] SetSubjectResponseServerError`. | +| AcceptGroupAdd | `GROUP_JID(iqTo)`; `` reads `acceptCode`, `acceptExpiration`, `acceptAdmin`. | Success with `membership_approval_request`, bare success, client error, server error. | `response.variants[2] AcceptGroupAddResponseClientError can shadow response.variants[3] AcceptGroupAddResponseServerError`. | + +Both result and error mixins compare the response's `id` and `from` with the +request's `id` and `to`. SetSubject's success has no operation payload beyond the +result envelope; it still requires those checks. The generated ID comes from the +base request mixin. Assertions are verified in the committed IR tests, but those +tests do not claim the generated parser enforces correlation. + +### Source provenance + +Revalidated against repository `1f5167c4cebf263edaf001e06a2497f213c55793` and +WhatsApp Web snapshot `2.3000.1047483476`, on 2026-10-07. Remote HEAD matched. +The reference report was checked against the bundle and current checkout. + +The exact [bundle-store asset](https://github.com/oxidezap/whatspec/releases/download/bundle-store/bundles-2.3000.1047483476-05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b.tar.xz) +was already present in the selected environment. `whatspec restore --from-lock +… --archive …` verified all 579 bundles against the committed lock. + +- Archive SHA-256: `22243b9c2ff18a66cea8fd66551821fe0f9f09786d107e2be8284637db86c349`. +- Bundle set hash: `05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b`. +- A: `65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55`. +- B: `09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8`. +- C: `34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2`. + +Modules were extracted statically with `wa_transform::extract_module_definitions`. +Offsets are byte intervals with exclusive ends. No `eval`, `vm`, sessions or +credentials were used to extract or test protocol behavior. + +| Module | Bundle | Span | +| --- | --- | --- | +| `WASmaxOutGroupsSetSubjectChangeSubjectMixin` | A | 27847..28186 | +| `WASmaxOutGroupsSetSubjectRequest` | A | 28188..28588 | +| `WASmaxGroupsSetSubjectRPC` | A | 28590..29826 | +| `WASmaxOutGroupsBaseIQSetRequestMixin` | B | 1138342..1138644 | +| `WASmaxOutGroupsBaseSetGroupMixin` | B | 1138646..1139064 | +| `WASmaxInGroupsIQResultResponseMixin` | B | 107120..107921 | +| `WASmaxInGroupsIQErrorResponseMixin` | B | 31289..32087 | +| `WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess` | C | 4229..4728 | +| `WASmaxInGroupsAcceptGroupAddResponseSuccess` | C | 5098..5459 | +| `WASmaxOutGroupsAcceptGroupAddRequest` | C | 5461..5920 | +| `WASmaxGroupsAcceptGroupAddRPC` | C | 5922..7533 | + +These are static observations of this preserved Web build, not proof of the +server's complete contract or current account behavior. + +## Validation and cost + +Four regression tests fail on the base commit and pass after the change: missing +contract, wholly discarded payload, an actual unrepresentable nested response +from `WAWebQueryBusinessCategoriesJob`, and ordered child-gated success. The +empty-outcome control remains successful. The pilot test reads the real IQ IR. +The whole generated file is syntax-checked, not type-checked against an external +consumer. The conformance front owns independent execution validation. + +Two in-process debug generations of all 142 IQ operations produced identical +1,204,604-byte outputs, SHA-256 +`4a4473e33d9e8fbcdf87d47783b0ea6c654fdd13243695918910e72c380fb50f`, +in 2.43 and 2.40 seconds. 69 parsers have an explicit rejection diagnostic. +The base emitter produced 1,523,859 bytes in 2.84 and 2.98 seconds in the same +debug setup. The smaller output mostly removes parsers that could not preserve +all outcomes; it is not an optimization claim. These are local generation +measurements, not runtime or release-build benchmarks. +The reference Rust file is ignored by git; no new checked-in generated catalog or +runtime dependency is required. Future updates must review the rejected operation +set as well as source changes, without relaxing admission to preserve a count. + +Local validation passed `cargo test --locked --workspace`, including 176 +`wa-codegen` tests, +`cargo clippy --locked -p wa-codegen --all-targets -- -D warnings`, formatting, +and all 12 real JSON Schemas. Full bundle regeneration with `--check` reported +27 committed artifacts up to date. `scripts/lint-ir.py` still fails on the +pre-existing exact-count baselines; its output is byte-identical on the base +commit and this branch. This patch changes neither IR nor baselines. From 1b9286d2d824fb12fd17f4fe4c1a4148e9074691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 21:50:48 +0000 Subject: [PATCH 04/52] ci: run offline conformance fixture checks independently --- .github/workflows/ci.yml | 13 +++++++++++++ tests/conformance/README.md | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57fef1c7..1992ca29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,19 @@ permissions: contents: read jobs: + conformance-fixtures: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + # Uses only the standard library and committed fixtures. No bundle restore + # or compiled extractor is needed for integrity and rejection checks. + - name: Verify conformance fixture integrity and capture rejection + run: python3 tests/conformance/test_capture.py -v + check: runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/tests/conformance/README.md b/tests/conformance/README.md index 8562f11f..b711d62e 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -96,6 +96,9 @@ cargo fmt --all -- --check The two Python tests check fixture span/hash identity and rejection of changed, missing, extra bundles, wrong setHash and wrong bundle count. They need no network or compiled extractor. The Rust tests use only committed fixtures. +The independent CI job `conformance-fixtures` runs +`python3 tests/conformance/test_capture.py -v` on every workflow run, including +when dependency checks fail in the separate `check` job. To review a new snapshot, first restore its exact lock from bundle-store, then: @@ -175,5 +178,6 @@ Current lint remains red on six *improved* exact baselines: unresolved IQ attribute paths 49 vs 55, child paths 10 vs 12, MEX operations without established presence 10 vs 13, undetermined MEX variables 106 vs 108, WAM constructions with no catalog entry 29 vs 41, and unread WAM arguments 93 vs 104. Baselines and CI -were not changed here. Published-SHA CI and combined emitter qualification are +were not changed here, apart from adding the independent fixture-test job. +Published-SHA CI and combined emitter qualification are still required before declaring the repository qualified. From 1fb26b8137b7ef5b9c2ad34009105aa524cd6ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 21:52:05 +0000 Subject: [PATCH 05/52] test: report verified schema pair counts explicitly --- docs/snapshot-quality-2026-10-07.md | 3 ++- scripts/tests/test_snapshot_validation.py | 8 ++++++++ scripts/validate-schemas.py | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index c31bec3b..cbfe21ca 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -28,7 +28,8 @@ All 12 real JSON documents in each snapshot pass their emitted schemas with `jsonschema 4.26.0`. The validator previously returned success for an entirely empty directory, printing 12 `skip` lines. It now fails missing documents, missing schemas, malformed JSON, invalid schemas, and unresolved external -references. Eight offline regression tests cover those cases and valid input. +references. Nine offline regression tests cover those cases, valid input, and an explicit +count of successfully validated pairs. Both snapshots reproduce all 27 checked artifacts with the same release binary and `update --bundles ... --wa-version ... --check`. The old run took 13.970 s diff --git a/scripts/tests/test_snapshot_validation.py b/scripts/tests/test_snapshot_validation.py index 49e548b6..24230981 100644 --- a/scripts/tests/test_snapshot_validation.py +++ b/scripts/tests/test_snapshot_validation.py @@ -31,6 +31,14 @@ def validate(self): def test_complete_snapshot(self): self.assertEqual(self.validate(), 0) + def test_summary_counts_only_successful_pairs(self): + (self.root / validator.DOMAINS[0][0]).unlink() + output = io.StringIO() + with contextlib.redirect_stdout(output): + result = validator.validate(self.root) + self.assertEqual(result, 1) + self.assertIn("11/12 schema/document pairs validated; 1 failed", output.getvalue()) + def test_missing_document_is_failure(self): (self.root / validator.DOMAINS[0][0]).unlink() self.assertEqual(self.validate(), 1) diff --git a/scripts/validate-schemas.py b/scripts/validate-schemas.py index 5c156bbc..03f057ab 100755 --- a/scripts/validate-schemas.py +++ b/scripts/validate-schemas.py @@ -65,6 +65,8 @@ def validate(root: Path) -> int: print(f" …and {len(errors) - 20} more") else: print(f"ok {doc_rel} conforms to {schema_rel}") + print(f"{len(DOMAINS) - failures}/{len(DOMAINS)} schema/document pairs validated; " + f"{failures} failed") return 1 if failures else 0 From 92bd61cb1613e02ab2f7a1d6f48b409f93c276f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 21:57:45 +0000 Subject: [PATCH 06/52] fix: bind delta reviews to both artifact hashes --- crates/whatspec/src/contract_diff.rs | 44 +++++++++++++++++++ docs/snapshot-quality-2026-10-07.md | 2 +- docs/snapshot-quality-2026-10-07.reviews.json | 4 +- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/crates/whatspec/src/contract_diff.rs b/crates/whatspec/src/contract_diff.rs index 8eb163c7..147da1d3 100644 --- a/crates/whatspec/src/contract_diff.rs +++ b/crates/whatspec/src/contract_diff.rs @@ -283,6 +283,10 @@ pub fn report(old: &Path, new: &Path, evidence: Option<&Path>) -> Result change["domain"] = json!(domain); change["oldSource"] = reference(a, &change["oldPath"]); change["newSource"] = reference(b, &change["newPath"]); + // A missing field still belongs to a concrete domain document. Bind + // that document even though there is no field pointer to reference. + change["oldArtifactSha256"] = json!(a.map(|(_, hash, _)| hash)); + change["newArtifactSha256"] = json!(b.map(|(_, hash, _)| hash)); // Bind a review to exact artifact contents AND inputs, not just a name // that can survive an unrelated future snapshot. change["oldSetHash"] = json!(old.lock.set_hash); @@ -389,6 +393,46 @@ pub fn run(args: &[String]) -> Result<()> { mod tests { use super::*; + #[test] + fn added_and_removed_fields_bind_both_document_hashes() { + let dir = std::env::temp_dir().join(format!("whatspec-absent-side-{}", std::process::id())); + let old = dir.join("old"); + let new = dir.join("new"); + let write = |root: &Path, operations: Value| { + std::fs::create_dir_all(root.join("mex")).unwrap(); + let lock = wa_store::lock::BundleLock::new("test", vec![]); + std::fs::write(root.join("bundles.lock.json"), lock.to_pretty_json()).unwrap(); + let doc = json!({"waVersion":"test", "schemaVersion":"4.3.0", "operations":operations}) + .to_string(); + std::fs::write(root.join("mex/index.json"), &doc).unwrap(); + let manifest = json!({"waVersion":"test", "schemaVersion":"4.3.0", "domains":{"mex":{"file":"mex/index.json","sha256":wa_text::sha256_hex(doc.as_bytes())}}}); + std::fs::write(root.join("manifest.json"), manifest.to_string()).unwrap(); + }; + write(&old, json!({"A":{}})); + write(&new, json!({"A":{"extra":true}})); + let added = report(&old, &new, None).unwrap()["changes"][0]["id"].clone(); + let removed = report(&new, &old, None).unwrap()["changes"][0]["id"].clone(); + // The field is still absent in this side, and both input locks are unchanged. + write(&old, json!({"A":{}, "Unrelated":{"docId":"9"}})); + let updated_added = report(&old, &new, None).unwrap(); + let updated_removed = report(&new, &old, None).unwrap(); + std::fs::remove_dir_all(dir).unwrap(); + let added_change = updated_added["changes"] + .as_array() + .unwrap() + .iter() + .find(|c| c["newPath"] == "/operations/A/extra") + .unwrap(); + let removed_change = updated_removed["changes"] + .as_array() + .unwrap() + .iter() + .find(|c| c["oldPath"] == "/operations/A/extra") + .unwrap(); + assert_ne!(added_change["id"], added); + assert_ne!(removed_change["id"], removed); + } + #[test] fn snapshot_hashes_and_versions_are_checked_before_diffing() { let dir = diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index cbfe21ca..dca6c104 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -122,7 +122,7 @@ whatspec diff old-generated generated --json \ --evidence docs/snapshot-quality-2026-10-07.reviews.json > contracts.json ``` -The raw report is 2,501,713 bytes. Full optional source indexes are 14,248,640 and +The raw report is 2,591,393 bytes. Full optional source indexes are 14,248,640 and 14,223,916 bytes, with 20,209 and 19,713 recovered module names. Only the focused sidecar and review ledger are committed. The generated IR is unchanged. diff --git a/docs/snapshot-quality-2026-10-07.reviews.json b/docs/snapshot-quality-2026-10-07.reviews.json index 9f5914ea..afd77453 100644 --- a/docs/snapshot-quality-2026-10-07.reviews.json +++ b/docs/snapshot-quality-2026-10-07.reviews.json @@ -2,7 +2,7 @@ "reportVersion": 1, "reviews": [ { - "id": "d8a128b58ba26a3247e9218bd8403bb47a8eb365e938ada5be9411e4b8179d8a", + "id": "44f5730a6c4282f12bd9200b9d8864a3e3beb75f7b3daa6fb0dc074a1d269aa0", "classification": "extraction-improvement", "basis": "The upstream helper changes from four positional arguments to one object. Its category attributes and repeated user child remain present. The new IR recovers name/value/dhash and users[] paths; the old user paths incorrectly passed through name. This does not establish full branch coverage.", "references": [ @@ -11,7 +11,7 @@ ] }, { - "id": "98a7106103385fb386e200e5319bb7432a7f1fc61d86e9a74a9cfc8d5d324613", + "id": "4c8e12058941be16491dc7de2abd60931c52c9405fa962eb8f262bc38972b3b0", "classification": "upstream-change", "basis": "The preserved facebookRelayOperation module exports literal 27747880408206174 in the old set and 27796221486653417 in the new set. Both values agree with their IR. This establishes a persisted operation ID change in these snapshots, not server compatibility.", "references": [ From 2408fbf369cd53bb857e630c6bad7c11d235d272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 21:51:59 +0000 Subject: [PATCH 07/52] fix: update rustls for RUSTSEC-2026-0285 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb27cc68..bba5a519 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1666,9 +1666,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -1689,9 +1689,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", From 585ff93e7c9980986d988d2b4bc10c0b05fb5157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 21:51:59 +0000 Subject: [PATCH 08/52] fix: update rustls for RUSTSEC-2026-0285 (cherry picked from commit 81d355cd7392872174201dee3617a480e7a9f5dd) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb27cc68..bba5a519 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1666,9 +1666,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -1689,9 +1689,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", From d408c846e3a999989dcc4b774ed724943fbabbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:07:13 -0300 Subject: [PATCH 09/52] fix(codegen): preserve pilot IQ outcomes and request correlation --- crates/wa-codegen/src/spec.rs | 486 +++++++++++++++++- .../wa-codegen/tests/fixtures/iq_runtime.rs | 357 +++++++++++++ crates/wa-codegen/tests/iq_runtime.rs | 59 +++ docs/iq-response-admission.md | 120 +++-- 4 files changed, 952 insertions(+), 70 deletions(-) create mode 100644 crates/wa-codegen/tests/fixtures/iq_runtime.rs create mode 100644 crates/wa-codegen/tests/iq_runtime.rs diff --git a/crates/wa-codegen/src/spec.rs b/crates/wa-codegen/src/spec.rs index 1d014cf6..fdb98775 100644 --- a/crates/wa-codegen/src/spec.rs +++ b/crates/wa-codegen/src/spec.rs @@ -232,8 +232,25 @@ fn pin_conditions(v: &ResponseVariant, node: &str) -> Vec { (a.kind == AssertionKind::Child) .then_some(a.name.as_deref()) .flatten() - .map(|name| format!("{node}.get_optional_child({}).is_some()", rust_lit(name))) + .map(|name| { + format!( + "{node}.get_children_by_tag({}).count() == 1", + rust_lit(name) + ) + }) + })); + conditions.extend(v.assertions.iter().filter_map(|a| { + if a.kind == AssertionKind::Tag { + a.name + .as_ref() + .map(|name| format!("{node}.tag == {}", rust_lit(name))) + } else { + None + } })); + if let Some(error) = error_selection(v, node) { + conditions.push(error); + } conditions } @@ -257,6 +274,275 @@ fn pins_can_coincide(mine: &[Pin<'_>], other: &[Pin<'_>]) -> bool { }) } +/// Error predicates are sets of (integer code, optional text pin). Compare unions +/// of intervals only; this does not model arbitrary parser control flow. +fn error_arms_cover(earlier: &[wa_ir::ErrorArm], later: &[wa_ir::ErrorArm]) -> bool { + fn band(arm: &wa_ir::ErrorArm) -> (i128, i128) { + let min = arm.code.or(arm.code_min).unwrap_or(i64::MIN); + let max = arm.code.or(arm.code_max).unwrap_or(i64::MAX); + (i128::from(min), i128::from(max)) + } + later.iter().all(|arm| { + let (mut cursor, end) = band(arm); + let mut bands: Vec<_> = earlier + .iter() + .filter(|a| a.text.is_none() || (arm.text.is_some() && a.text == arm.text)) + .map(band) + .collect(); + bands.sort_unstable(); + for (lo, hi) in bands { + if lo > cursor { + break; + } + cursor = cursor.max(hi + 1); + if cursor > end { + return true; + } + } + cursor > end + }) +} + +/// The direct, bounded code/text error union observed in the pilots. Unknown +/// payload structures use the ordinary admission path, never this specialization. +fn direct_error_payload(v: &ResponseVariant) -> Option<&wa_ir::ParsedField> { + use wa_ir::ParsedFieldType; + if v.error_arms.is_empty() || v.error_envelope.is_some() || v.fields.len() != 2 { + return None; + } + let payload = v.fields.iter().find(|f| { + f.parser_required + && f.source_path.as_deref() == Some(&["error".to_string()]) + && f.field_type == ParsedFieldType::Union + })?; + if !v.fields.iter().any(|f| { + f.method == "attrString" + && f.parser_required + && f.wire_name.as_deref().unwrap_or(&f.name) == "type" + && f.literal_value.as_deref() == Some("error") + }) { + return None; + } + let variants = payload.union_variants.as_ref()?; + if crate::union::classify_union(payload).is_none() || variants.len() != v.error_arms.len() { + return None; + } + for (variant, arm) in variants.iter().zip(&v.error_arms) { + let lo = arm.code.or(arm.code_min)?; + let hi = arm.code.or(arm.code_max)?; + // This decoder only needs exact small integers. Values outside the band + // cannot round into it under the source's JS parseInt/Number semantics. + if lo < 0 + || hi > i64::from(i32::MAX) + || lo > hi + || arm.name.as_deref() != Some(&variant.name) + { + return None; + } + let code = variant + .fields + .iter() + .find(|f| f.wire_name.as_deref().unwrap_or(&f.name) == "code")?; + let text = variant + .fields + .iter() + .find(|f| f.wire_name.as_deref().unwrap_or(&f.name) == "text")?; + if code.method != "attrInt" + || !code.parser_required + || code.field_type != ParsedFieldType::Integer + || code + .literal_value + .as_ref() + .and_then(|s| s.parse::().ok()) + != arm.code + || code.int_min != arm.code_min + || code.int_max != arm.code_max + || text.method != "attrString" + || !text.parser_required + || text.literal_value != arm.text + { + return None; + } + // Apart from code/text, the pilot has optional . + // No unbounded coercion, repeated tree or opaque union is inferred here. + if variant.fields.iter().any(|f| { + let wire = f.wire_name.as_deref().unwrap_or(&f.name); + if wire == "code" || wire == "text" { + return false; + } + !(f.method == "child" + && !f.parser_required + && f.repeats != Some(true) + && f.source_path.is_none() + && f.children.as_ref().is_some_and(|children| { + children.iter().all(|c| { + c.method == "attrString" && c.source_path.is_none() && c.children.is_none() + }) + })) + }) { + return None; + } + } + Some(payload) +} + +fn error_arm_condition(arm: &wa_ir::ErrorArm) -> String { + let mut terms = Vec::new(); + if let Some(code) = arm.code { + terms.push(format!("code == {code}i64")); + } + if let Some(min) = arm.code_min { + terms.push(format!("code >= {min}i64")); + } + if let Some(max) = arm.code_max { + terms.push(format!("code <= {max}i64")); + } + if let Some(text) = &arm.text { + terms.push(format!( + "error.get_attr(\"text\").map(|v| v.as_str()).as_deref() == Some({})", + rust_lit(text) + )); + } + format!("({})", terms.join(" && ")) +} + +fn error_selection(v: &ResponseVariant, node: &str) -> Option { + direct_error_payload(v)?; + let arms: Vec<_> = v.error_arms.iter().map(error_arm_condition).collect(); + Some(format!( + "({node}.get_children_by_tag(\"error\").count() == 1 && {node}.get_optional_child(\"error\").is_some_and(|error| error.get_attr(\"text\").is_some() && error.get_attr(\"code\").and_then(|v| __iq_error_code(v.as_str())).is_some_and(|code| {})))", + arms.join(" || ") + )) +} + +/// Source WASmaxParseUtils uses parseInt(value, 10), not Rust's whole-string +/// integer parse. Overflow is outside every admitted error-code band. +fn emit_error_code_decoder(indent: &str) -> Vec { + r#"fn __iq_error_code(value: &str) -> Option { + let value = value.trim_start_matches(|c| matches!(c, '\u{0009}'..='\u{000d}' | ' ' | '\u{00a0}' | '\u{1680}' | '\u{2000}'..='\u{200a}' | '\u{2028}' | '\u{2029}' | '\u{202f}' | '\u{205f}' | '\u{3000}' | '\u{feff}')); + let offset = usize::from(matches!(value.as_bytes().first(), Some(b'+') | Some(b'-'))); + let end = offset + value.bytes().skip(offset).take_while(u8::is_ascii_digit).count(); + if end == offset { None } else { value[..end].parse().ok() } +}"#.lines().map(|line| format!("{indent}{line}")).collect() +} + +fn emit_direct_error_parser( + v: &ResponseVariant, + payload: &wa_ir::ParsedField, + sname: &str, + indent: &str, +) -> Vec { + let ename = crate::union::enum_name(payload, sname); + let mut lines = vec![ + format!( + "{indent}let error = response.get_optional_child(\"error\").ok_or_else(|| anyhow::anyhow!(\"missing error\"))?;" + ), + format!( + "{indent}let code = error.get_attr(\"code\").and_then(|v| __iq_error_code(v.as_str())).ok_or_else(|| anyhow::anyhow!(\"invalid error code\"))?;" + ), + format!("{indent}let payload = (|| -> Result<{ename}, anyhow::Error> {{"), + ]; + for (variant, arm) in payload + .union_variants + .as_ref() + .unwrap() + .iter() + .zip(&v.error_arms) + { + let vname = pascal_case(&variant.name); + let struct_name = format!("{ename}{vname}"); + let code_field = variant + .fields + .iter() + .find(|f| f.wire_name.as_deref().unwrap_or(&f.name) == "code") + .unwrap(); + let fields: Vec<_> = variant + .fields + .iter() + .filter(|f| f.wire_name.as_deref().unwrap_or(&f.name) != "code") + .cloned() + .collect(); + let mut conditions = vec![error_arm_condition(arm)]; + for child in fields.iter().filter(|f| f.method == "child") { + conditions.push(format!( + "error.get_children_by_tag({}).count() <= 1", + rust_lit(child.tag.as_deref().unwrap_or(&child.name)) + )); + } + lines.push(format!("{indent} if {} {{", conditions.join(" && "))); + lines.push(format!( + "{indent} let parsed = (|| -> Result<{struct_name}, anyhow::Error> {{" + )); + lines.extend(crate::emit::emit_struct_parser( + &fields, + "error", + &struct_name, + &format!("{indent} "), + &struct_name, + )); + lines.push(format!("{indent} }})();")); + lines.push(format!("{indent} if let Ok(mut value) = parsed {{ value.{} = code as {}; return Ok({ename}::{vname}(value)); }}", rust_ident(&code_field.name), crate::fields::integer_width(code_field))); + lines.push(format!("{indent} }}")); + } + lines.push(format!( + "{indent} anyhow::bail!(\"no error payload matched\")" + )); + lines.push(format!("{indent}}})()?;")); + let header = v + .fields + .iter() + .find(|f| f.field_type != wa_ir::ParsedFieldType::Union) + .unwrap(); + lines.push(format!( + "{indent}Ok({sname} {{ {}: \"error\".to_string(), {}: Some(payload) }})", + rust_ident(&header.name), + rust_ident(&payload.name) + )); + lines +} + +/// The context entry point currently supports the two direct wire echoes proven +/// by the pilots. Other reference paths are diagnosed rather than guessed. +fn correlation_requirements(op: &IqStanzaDef) -> Result, String> { + fn references(assertions: &[wa_ir::ResponseAssertion]) -> Result, String> { + let mut out = Vec::new(); + for a in assertions + .iter() + .filter(|a| a.kind == AssertionKind::Reference) + { + match (a.name.as_deref(), a.reference_path.as_deref()) { + (Some("id"), Some(path)) if path == ["id"] => out.push(("id", "request_id")), + (Some("from"), Some(path)) if path == ["to"] => out.push(("from", "request_to")), + _ => { + return Err( + "guards.reference_unsupported: expected id=request.id or from=request.to" + .into(), + ); + } + } + } + out.sort_unstable(); + out.dedup(); + Ok(out) + } + let mut common = references(&op.response.assertions)?; + if let Some(first) = op.response.variants.first() { + let required = references(&first.assertions)?; + for variant in &op.response.variants[1..] { + if references(&variant.assertions)? != required { + return Err( + "guards.reference_nonuniform: outcome references require distinct contexts" + .into(), + ); + } + } + common.extend(required); + } + common.sort_unstable(); + common.dedup(); + Ok(common) +} + fn emit_outcome_types( op: &IqStanzaDef, spec_base: &str, @@ -289,6 +575,15 @@ fn emit_outcome_types( names.push((vname.clone(), format!("{spec_base}{vname}"))); } for (index, (variant, (_, name))) in op.response.variants.iter().zip(&names).enumerate() { + if variant.fields.iter().any(|field| { + field.field_type == wa_ir::ParsedFieldType::Union + && crate::union::classify_union(field).is_none() + }) { + return Err(format!( + "outcomes.unemittable: response.variants[{index}] {} has an unsupported payload union", + variant.tag + )); + } if !parser_is_valid(&variant.fields, name, name) { return Err(format!( "outcomes.unemittable: response.variants[{index}] {} has an unemittable payload parser", @@ -302,10 +597,9 @@ fn emit_outcome_types( // its captured discriminator assertions (e.g. `type:"result"`). An earlier variant // shadows a later one only when its required fields are a subset AND no conflicting // assertion sets them apart — then a later-response would match the earlier arm - // first (misclassification). When that can happen, fall back to the single-shape - // path rather than emit a parser that lies. (Variants distinguished only by an - // uncaptured discriminator — e.g. two errors differing by `` code — stay - // here; recovering those needs deeper child-discriminator capture.) + // first (misclassification). Reject fully covered later outcomes. The bounded + // direct error payload admits demonstrated code/text discrimination; other + // overlapping payload shapes remain unsupported. let req: Vec> = op .response .variants @@ -329,6 +623,12 @@ fn emit_outcome_types( // assertion, e.g. `type:"result"` vs `type:"error"`) sets them apart. if req[i].is_subset(&req[j]) && !assertions_conflict(&op.response.variants[i], &op.response.variants[j]) + && !(error_selection(&op.response.variants[i], "response").is_some() + && error_selection(&op.response.variants[j], "response").is_some() + && !error_arms_cover( + &op.response.variants[i].error_arms, + &op.response.variants[j].error_arms, + )) { return Err(format!( "outcomes.unemittable: response.variants[{i}] {} can shadow response.variants[{j}] {}", @@ -338,6 +638,31 @@ fn emit_outcome_types( } } + // Several preceding error outcomes can collectively cover a later one even + // when no single outcome does. Only combine roots with the same assertions. + for j in 1..op.response.variants.len() { + let later = &op.response.variants[j]; + if error_selection(later, "response").is_none() { + continue; + } + let preceding: Vec<_> = op.response.variants[..j] + .iter() + .enumerate() + .filter(|(i, earlier)| { + req[*i].is_subset(&req[j]) + && earlier.assertions == later.assertions + && error_selection(earlier, "response").is_some() + }) + .flat_map(|(_, earlier)| earlier.error_arms.iter().cloned()) + .collect(); + if !preceding.is_empty() && error_arms_cover(&preceding, &later.error_arms) { + return Err(format!( + "outcomes.unemittable: preceding error outcomes cover response.variants[{j}] {}", + later.tag + )); + } + } + let mut info: Vec<(String, String)> = Vec::new(); let mut seen_struct: HashSet = HashSet::new(); for (v, (vname, struct_name)) in op.response.variants.iter().zip(names) { @@ -415,6 +740,14 @@ fn emit_outcome_parse( indent: &str, ) -> Vec { let mut lines = Vec::new(); + if op + .response + .variants + .iter() + .any(|v| direct_error_payload(v).is_some()) + { + lines.extend(emit_error_code_decoder(indent)); + } for (i, (v, (vname, sname))) in op.response.variants.iter().zip(info).enumerate() { // The discriminator SELECTS; it does not bail. Spelled as a bail inside the payload // closure, a pin miss and a malformed payload were one `Err` and both moved on — so a @@ -457,13 +790,29 @@ fn emit_outcome_parse( lines.push(format!( "{body}let __r: Result<{sname}, anyhow::Error> = (|| -> Result<{sname}, anyhow::Error> {{" )); - lines.extend(emit_response_parser( - &v.fields, - sname, - &format!("{body} "), - sname, - )); + if let Some(payload) = direct_error_payload(v) { + lines.extend(emit_direct_error_parser( + v, + payload, + sname, + &format!("{body} "), + )); + } else { + lines.extend(emit_response_parser( + &v.fields, + sname, + &format!("{body} "), + sname, + )); + } lines.push(format!("{body}}})();")); + for field in v + .fields + .iter() + .filter(|f| f.parser_required && f.field_type == wa_ir::ParsedFieldType::Union) + { + lines.push(format!("{body}let __r = __r.and_then(|value| {{ if value.{}.is_none() {{ anyhow::bail!({}); }} Ok(value) }});", rust_ident(&field.name), rust_lit(&format!("{}: required payload {} did not match", v.tag, field.name)))); + } if unique { lines.push(format!("{body}return Ok({enum_name}::{vname}(__r?));")); } else { @@ -784,7 +1133,10 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - // A missing shape is not evidence of an empty successful result. Preserve the // existing associated type for source compatibility, but make degradation an // explicit error. An explicit empty outcome still uses its admitted union. - let rejection = if let Some(reason) = outcome_error.as_deref() { + let correlation = correlation_requirements(op); + let rejection = if let Err(reason) = &correlation { + Some(reason.as_str()) + } else if let Some(reason) = outcome_error.as_deref() { Some(reason) } else if !use_union && op.response.fields.is_empty() { Some("response.contract_missing: no recovered response fields or outcomes") @@ -802,7 +1154,39 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - } else { "response" }; - if let Some(reason) = rejection { + let needs_context = correlation.as_ref().is_ok_and(|refs| !refs.is_empty()); + if needs_context && rejection.is_none() { + lines.push(" fn parse_response(&self, _response: &wacore_binary::NodeRef<'_>) -> Result {".to_string()); + lines.push(format!( + " anyhow::bail!({})", + rust_lit(&format!( + "{}: guards.request_context_required: use parse_response_with_request", + op.module_name + )) + )); + lines.push(" }".into()); + lines.push("}".into()); + lines.push(format!("impl {spec_name} {{")); + lines.push(format!(" pub fn parse_response_with_request(&self, response: &wacore_binary::NodeRef<'_>, request_id: &str, request_to: &str) -> Result<{response_type_name}, anyhow::Error> {{")); + for (wire, value) in correlation.as_ref().unwrap() { + lines.push(format!(" if response.get_attr({}).map(|v| v.as_str()).as_deref() != Some({value}) {{ anyhow::bail!({}); }}", rust_lit(wire), rust_lit(&format!("{}: response.{wire} does not match {value}", op.module_name)))); + } + if use_union { + lines.extend(emit_outcome_parse( + op, + &outcome_info, + &response_type_name, + " ", + )); + } else { + lines.extend(emit_response_parser( + &op.response.fields, + &response_type_name, + " ", + child_prefix, + )); + } + } else if let Some(reason) = rejection { lines.push(" fn parse_response(&self, _response: &wacore_binary::NodeRef<'_>) -> Result {".to_string()); lines.push(format!( " anyhow::bail!({})", @@ -1529,7 +1913,9 @@ mod tests { "the two success outcomes must remain distinguishable: {code}" ); assert!( - code.contains("response.get_optional_child(\"membership_approval_request\").is_some()"), + code.contains( + "response.get_children_by_tag(\"membership_approval_request\").count() == 1" + ), "{code}" ); assert!( @@ -2105,6 +2491,66 @@ mod tests { ); } + #[test] + fn error_admission_preserves_text_pairs_ranges_and_fallback_order() { + fn exact(code: i64, text: &str) -> wa_ir::ErrorArm { + wa_ir::ErrorArm { + code: Some(code), + text: Some(text.into()), + ..Default::default() + } + } + fn range(lo: i64, hi: i64) -> wa_ir::ErrorArm { + wa_ir::ErrorArm { + code_min: Some(lo), + code_max: Some(hi), + ..Default::default() + } + } + let client = vec![ + exact(304, "already-exists"), + exact(500, "resource-constraint"), + range(400, 499), + ]; + let server = vec![exact(500, "internal-server-error"), range(500, 599)]; + assert!(!error_arms_cover(&client, &server)); + assert!(!error_arms_cover(&server, &client)); + assert!(error_arms_cover( + &[range(500, 599)], + &[exact(500, "resource-constraint")] + )); + assert!(!error_arms_cover( + &[exact(500, "resource-constraint")], + &[range(500, 599)] + )); + assert!(error_arms_cover( + &[range(400, 449), range(450, 499)], + &[range(400, 499)] + )); + assert!(!error_arms_cover( + &[range(400, 449), range(451, 499)], + &[range(400, 499)] + )); + assert!(error_arms_cover( + &[range(i64::MIN, i64::MAX)], + &[range(i64::MIN, i64::MAX)] + )); + } + + #[test] + fn unknown_reference_paths_are_diagnosed_not_ignored() { + let mut op = stanza("UnknownReference", None); + op.response.assertions.push(wa_ir::ResponseAssertion { + kind: AssertionKind::Reference, + name: Some("from".into()), + value: None, + reference_path: Some(vec!["account".into(), "to".into()]), + }); + let code = generate_spec(&op, "TEST_NAMESPACE", "UnknownReferenceSpec"); + assert!(code.contains("guards.reference_unsupported")); + assert!(!code.contains("pub fn parse_response_with_request")); + } + #[test] fn missing_response_contract_is_not_a_confirmation() { let op = stanza("MissingResponse", None); @@ -2152,7 +2598,7 @@ mod tests { } #[test] - fn real_pilots_preserve_requests_but_do_not_claim_complete_outcomes() { + fn real_pilots_emit_ordered_outcomes_with_required_request_context() { let ir: wa_ir::IqIr = serde_json::from_str(include_str!("../../../generated/iq/index.json")).unwrap(); for pilot in ["SetSubject", "AcceptGroupAdd"] { @@ -2178,9 +2624,13 @@ mod tests { } let code = generate_spec(op, "W_G2_NAMESPACE", &format!("{pilot}Spec")); assert!(code.contains("self.target.clone()")); - assert!(code.contains("outcomes.unemittable")); - assert!(code.contains(&format!("{pilot}ResponseClientError can shadow"))); - assert!(code.contains(&format!("{pilot}ResponseServerError"))); + assert!(!code.contains("outcomes.unemittable"), "{code}"); + assert!(code.contains("pub fn parse_response_with_request")); + assert!(code.contains("guards.request_context_required")); + assert!(code.contains("Some(request_id)")); + assert!(code.contains("Some(request_to)")); + assert!(code.contains("::ClientError(")); + assert!(code.contains("::ServerError(")); if pilot == "SetSubject" { assert_eq!( op.request.children[0] diff --git a/crates/wa-codegen/tests/fixtures/iq_runtime.rs b/crates/wa-codegen/tests/fixtures/iq_runtime.rs new file mode 100644 index 00000000..5e255153 --- /dev/null +++ b/crates/wa-codegen/tests/fixtures/iq_runtime.rs @@ -0,0 +1,357 @@ +#![allow(dead_code, unused_imports)] +// Deliberately small adapter for the existing reference emitter. No external +// client dependency: attributes and child lookup are ordinary in-memory values. +extern crate self as wacore_binary; +use std::collections::BTreeMap; +#[derive(Debug, Clone, Default)] +pub struct Node { + pub tag: String, + pub attrs: BTreeMap, + pub children: Vec, + pub bytes: Option>, +} +#[derive(Clone, Copy)] +pub struct NodeRef<'a> { + pub tag: &'a str, + node: &'a Node, +} +impl Node { + fn new(tag: &str, attrs: &[(&str, &str)], children: Vec) -> Self { + Self { + tag: tag.into(), + attrs: attrs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + children, + bytes: None, + } + } + fn as_ref(&self) -> NodeRef<'_> { + NodeRef { + tag: &self.tag, + node: self, + } + } +} +impl<'a> NodeRef<'a> { + pub fn get_attr(&self, name: &str) -> Option<&'a String> { + self.node.attrs.get(name) + } + pub fn get_optional_child(&self, tag: &str) -> Option> { + self.node + .children + .iter() + .find(|n| n.tag == tag) + .map(Node::as_ref) + } + pub fn get_children_by_tag(&self, tag: &'a str) -> impl Iterator> { + self.node + .children + .iter() + .filter(move |n| n.tag == tag) + .map(Node::as_ref) + } + pub fn content_str(&self) -> Option<&'a str> { + self.node + .bytes + .as_ref() + .and_then(|b| std::str::from_utf8(b).ok()) + } +} +pub mod jid { + #[derive(Debug, Clone)] + pub enum Server { + Pn, + Group, + } + #[derive(Debug, Clone)] + pub struct Jid(pub String); + impl Jid { + pub fn new(user: &str, server: Server) -> Self { + let server = match server { + Server::Pn => "s.whatsapp.net", + Server::Group => "g.us", + }; + Self(if user.is_empty() { + server.into() + } else { + format!("{user}@{server}") + }) + } + } + impl std::fmt::Display for Jid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } +} +pub mod node { + #[derive(Debug, Clone)] + pub enum NodeContent { + Nodes(Vec), + } +} +pub mod builder { + pub struct NodeBuilder(crate::Node); + impl NodeBuilder { + pub fn new(tag: &str) -> Self { + Self(crate::Node { + tag: tag.into(), + ..Default::default() + }) + } + pub fn attr(mut self, name: &str, value: impl std::fmt::Display) -> Self { + self.0.attrs.insert(name.into(), value.to_string()); + self + } + pub fn bytes(mut self, bytes: Vec) -> Self { + self.0.bytes = Some(bytes); + self + } + pub fn build(self) -> crate::Node { + self.0 + } + } +} +pub mod request { + pub struct InfoQuery<'a> { + pub namespace: &'a str, + pub target: crate::jid::Jid, + pub content: Option, + pub kind: &'static str, + } + impl<'a> InfoQuery<'a> { + pub fn set( + namespace: &'a str, + target: crate::jid::Jid, + content: Option, + ) -> Self { + Self { + namespace, + target, + content, + kind: "set", + } + } + pub fn get( + namespace: &'a str, + target: crate::jid::Jid, + content: Option, + ) -> Self { + Self { + namespace, + target, + content, + kind: "get", + } + } + } +} +pub mod iq { + pub mod spec { + pub trait IqSpec { + type Response; + fn build_iq(&self) -> crate::request::InfoQuery<'static>; + fn parse_response( + &self, + response: &crate::NodeRef<'_>, + ) -> Result; + } + } +} +#[path = "generated.rs"] +mod generated; +use generated::w_g2::*; +use iq::spec::IqSpec; +fn result(children: Vec) -> Node { + Node::new( + "iq", + &[("type", "result"), ("id", "req-1"), ("from", "123@g.us")], + children, + ) +} +fn error(code: &str, text: Option<&str>) -> Node { + let mut child = Node::new("error", &[("code", code)], vec![]); + if let Some(text) = text { + child.attrs.insert("text".into(), text.into()); + } + let mut node = result(vec![child]); + node.attrs.insert("type".into(), "error".into()); + node +} +fn main() { + let target = jid::Jid::new("123", jid::Server::Group); + let subject = MakeSetSubjectRequestSpec::new(target.clone(), b"new subject".to_vec()); + let accept = MakeAcceptGroupAddRequestSpec::new( + "invite", + 12345, + &jid::Jid::new("456", jid::Server::Pn), + target, + ); + let request = subject.build_iq(); + assert_eq!(request.target.0, "123@g.us"); + assert_eq!(request.namespace, "w:g2"); + assert_eq!(request.kind, "set"); + let node::NodeContent::Nodes(nodes) = request.content.unwrap(); + assert_eq!(nodes[0].tag, "subject"); + assert_eq!(nodes[0].bytes.as_deref(), Some(b"new subject".as_slice())); + let request = accept.build_iq(); + assert_eq!(request.target.0, "123@g.us"); + let node::NodeContent::Nodes(nodes) = request.content.unwrap(); + assert_eq!(nodes[0].attrs["code"], "invite"); + assert_eq!(nodes[0].attrs["expiration"], "12345"); + assert_eq!(nodes[0].attrs["admin"], "456@s.whatsapp.net"); + let bare = result(vec![]); + assert!(subject.parse_response(&bare.as_ref()).is_err()); + assert!(accept.parse_response(&bare.as_ref()).is_err()); + assert!(matches!( + subject + .parse_response_with_request(&bare.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakeSetSubjectRequestResponse::Success(_) + )); + assert!(matches!( + accept + .parse_response_with_request(&bare.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakeAcceptGroupAddRequestResponse::Success(_) + )); + let approval = result(vec![Node::new("membership_approval_request", &[], vec![])]); + assert!(matches!( + accept + .parse_response_with_request(&approval.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakeAcceptGroupAddRequestResponse::GroupJoinRequestSuccess(_) + )); + let extension = result(vec![Node::new("future_extension", &[], vec![])]); + assert!(matches!( + accept + .parse_response_with_request(&extension.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakeAcceptGroupAddRequestResponse::Success(_) + )); + for (code, text, client) in [ + ("304", "already-exists", true), + ("400", "bad-request", true), + ("499", "future-client-error", true), + ("500", "resource-constraint", true), + ("500", "internal-server-error", false), + ("500", "future-server-error", false), + ("599", "future-server-error", false), + ] { + let node = error(code, Some(text)); + let parsed = accept + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap(); + assert_eq!( + matches!(parsed, MakeAcceptGroupAddRequestResponse::ClientError(_)), + client, + "{code} {text}" + ); + if !client { + assert!(matches!( + parsed, + MakeAcceptGroupAddRequestResponse::ServerError(_) + )); + } + } + for (code, text) in [ + ("304", Some("wrong-text")), + ("399", Some("unknown")), + ("600", Some("unknown")), + ("not-int", Some("unknown")), + ("400", None), + ] { + let node = error(code, text); + assert!( + accept + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .is_err(), + "{code} {text:?}" + ); + } + for encoded in ["0304", "+304", " 304tail", "\u{feff}304", "304.9", "304e7"] { + let node = error(encoded, Some("already-exists")); + assert!( + matches!( + accept + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakeAcceptGroupAddRequestResponse::ClientError(_) + ), + "{encoded}" + ); + } + let mut duplicate = error("500", Some("internal-server-error")); + duplicate.children.push(duplicate.children[0].clone()); + assert!( + accept + .parse_response_with_request(&duplicate.as_ref(), "req-1", "123@g.us") + .is_err() + ); + let duplicate_approval = result(vec![ + Node::new("membership_approval_request", &[], vec![]), + Node::new("membership_approval_request", &[], vec![]), + ]); + assert!(matches!( + accept + .parse_response_with_request(&duplicate_approval.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakeAcceptGroupAddRequestResponse::Success(_) + )); + for wire in ["id", "from"] { + let mut invalid = bare.clone(); + invalid.attrs.remove(wire); + assert!( + accept + .parse_response_with_request(&invalid.as_ref(), "req-1", "123@g.us") + .is_err() + ); + invalid.attrs.insert(wire.into(), "wrong".into()); + assert!( + accept + .parse_response_with_request(&invalid.as_ref(), "req-1", "123@g.us") + .is_err() + ); + } + for code in ["400", "499", "500", "599"] { + let node = error(code, Some("unknown-text")); + let parsed = subject + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap(); + match parsed { + MakeSetSubjectRequestResponse::ClientError(value) => { + assert!(value.error_set_subject_client_errors.is_some()) + } + MakeSetSubjectRequestResponse::ServerError(value) => { + assert!(value.error_server_errors.is_some()) + } + _ => panic!("error must never become success"), + } + } + assert!( + subject + .parse_response_with_request(&bare.as_ref(), "other-id", "123@g.us") + .is_err() + ); + assert!( + subject + .parse_response_with_request(&bare.as_ref(), "req-1", "999@g.us") + .is_err() + ); + let mut invalid = bare.clone(); + invalid.tag = "message".into(); + assert!( + accept + .parse_response_with_request(&invalid.as_ref(), "req-1", "123@g.us") + .is_err() + ); + invalid = bare; + invalid.attrs.insert("type".into(), "set".into()); + assert!( + accept + .parse_response_with_request(&invalid.as_ref(), "req-1", "123@g.us") + .is_err() + ); +} diff --git a/crates/wa-codegen/tests/iq_runtime.rs b/crates/wa-codegen/tests/iq_runtime.rs new file mode 100644 index 00000000..5f8360a5 --- /dev/null +++ b/crates/wa-codegen/tests/iq_runtime.rs @@ -0,0 +1,59 @@ +//! Compile and execute the emitted pilot parsers against a small in-memory node +//! adapter. This checks generated Rust, not a binary codec or external client API. +use std::{fs, process::Command}; + +#[test] +fn generated_pilot_requests_and_responses_execute() { + let mut ir: wa_ir::IqIr = + serde_json::from_str(include_str!("../../../generated/iq/index.json")).unwrap(); + ir.stanzas.retain(|op| { + matches!( + op.module_name.as_str(), + "WASmaxOutGroupsSetSubjectRequest" | "WASmaxOutGroupsAcceptGroupAddRequest" + ) + }); + assert_eq!(ir.stanzas.len(), 2); + let dir = std::env::temp_dir().join(format!("whatspec-iq-runtime-{}", std::process::id())); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("generated.rs"), wa_codegen::generate_iq(&ir)).unwrap(); + fs::write(dir.join("main.rs"), include_str!("fixtures/iq_runtime.rs")).unwrap(); + let deps = std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .to_path_buf(); + let anyhow = fs::read_dir(&deps) + .unwrap() + .map(|e| e.unwrap().path()) + .find(|p| { + p.file_name() + .unwrap() + .to_string_lossy() + .starts_with("libanyhow-") + && p.extension().is_some_and(|ext| ext == "rlib") + }) + .expect("anyhow dev dependency rlib"); + let compile = Command::new("rustc") + .arg("--edition=2024") + .arg(dir.join("main.rs")) + .arg("--extern") + .arg(format!("anyhow={}", anyhow.display())) + .arg("-L") + .arg(format!("dependency={}", deps.display())) + .arg("-o") + .arg(dir.join("run")) + .output() + .unwrap(); + assert!( + compile.status.success(), + "generated pilot compilation failed:\n{}", + String::from_utf8_lossy(&compile.stderr) + ); + let run = Command::new(dir.join("run")).output().unwrap(); + assert!( + run.status.success(), + "generated pilot execution failed:\n{}", + String::from_utf8_lossy(&run.stderr) + ); + fs::remove_dir_all(dir).unwrap(); +} diff --git a/docs/iq-response-admission.md b/docs/iq-response-admission.md index afc475d8..7e2443b4 100644 --- a/docs/iq-response-admission.md +++ b/docs/iq-response-admission.md @@ -1,6 +1,6 @@ # IQ response generation admission -This is the first response-emitter change, not a claim of complete IQ generation. +This change admits the two verified pilots, not every possible IQ contract. `wa-codegen` 0.2.0 stops reporting success when response generation loses a contract, payload, or outcome. It does not change the language-neutral IQ schema or add a runtime dependency. The schema remains 4.3.0. @@ -20,6 +20,8 @@ certified complete. | `response.contract_missing` | No recovered response fields or explicit outcomes. Empty metadata does not prove an empty successful response. | | `response.payload_unemittable` | Recovered fields produced no response payload. | | `response.parser_unemittable` | The generated initializer cannot represent the recovered payload. | +| `guards.reference_unsupported` / `guards.reference_nonuniform` | A recovered request reference cannot use the supported uniform wire context. | +| `guards.request_context_required` | Call `parse_response_with_request` with the actual request ID and target. | | `outcomes.unemittable` | A variant parser cannot be emitted, or an earlier variant can shadow a later one under the emitter's known predicates. The message names variant indexes and tags. | An explicit, admitted empty outcome still produces a successful empty outcome @@ -52,35 +54,44 @@ a proposed interface, **not implemented by the response-only constant above**. | --- | --- | --- | | Request | Known target, argument mapping, content, child cardinality and variants; no invented inputs. | Existing request emitter preserved; pilot mappings tested. No general strict request admission. | | Response | All recovered payload fields emitted; explicit empty outcome distinguished from missing extraction. | Missing/fully discarded payload and invalid initializer fail explicitly. This is not a recursive completeness proof. | -| Outcomes | Source order retained and every supported outcome reachable. No primary-success fallback. | Rejected unions fail explicitly. Child presence is handled. Error-arm discrimination remains a pilot blocker. | -| Constraints | Literal values, ranges, enum policy, error code/text pairings and reference constraints retained. | No new global constraint-admission claim. Existing `pending_drops` is not serialized, so it cannot prove per-operation recovery from saved IR. | -| Guards | Only demonstrated guards necessary for the selected operation. Request correlation must receive the actual request context. | Child-presence guard added. Correlation is not implemented in this patch; no universal expression AST is introduced. | - -Proposed correlation input is the actual request's `id` and `to`, independent of -any caller's transport, event model or application API. The legacy response -method has no generated request ID. A follow-up must define a context-taking -entry point before claiming checks of `response.id == request.id` and -`response.from == request.to`. Do not infer these from an unrelated request or -hardcode the server address. Any IR/schema additions for recovery status need -coordination with the snapshot and conformance fronts; none are made here. - -## Pilot findings - -Both pilots remain rejected by full outcome admission in this patch. The gate -cannot distinguish their client-error and server-error arms using its current -signature, although the IR carries their error vocabularies. Admitting only the -success variants would conceal that limitation. - -| Operation | Request evidence | Ordered outcomes | Current emitter diagnostic | -| --- | --- | --- | --- | -| SetSubject | `GROUP_JID(iqTo)`; `` content is `subjectElementValue`. | Success, client error, server error. | `response.variants[1] SetSubjectResponseClientError can shadow response.variants[2] SetSubjectResponseServerError`. | -| AcceptGroupAdd | `GROUP_JID(iqTo)`; `` reads `acceptCode`, `acceptExpiration`, `acceptAdmin`. | Success with `membership_approval_request`, bare success, client error, server error. | `response.variants[2] AcceptGroupAddResponseClientError can shadow response.variants[3] AcceptGroupAddResponseServerError`. | - -Both result and error mixins compare the response's `id` and `from` with the -request's `id` and `to`. SetSubject's success has no operation payload beyond the -result envelope; it still requires those checks. The generated ID comes from the -base request mixin. Assertions are verified in the committed IR tests, but those -tests do not claim the generated parser enforces correlation. +| Outcomes | Source order retained and every supported outcome reachable. No primary-success fallback. | Rejected unions fail explicitly. Child presence is handled. Bounded direct code/text error unions preserve source order and distinguish partially overlapping outcomes; fully covered later outcomes are rejected. | +| Constraints | Literal values, ranges, enum policy, error code/text pairings and reference constraints retained. | Pilot code/text pairs, decimal-prefix integer coercion, inclusive ranges and unique selected children are enforced. No global constraint-admission claim. Existing `pending_drops` is not serialized, so it cannot prove per-operation recovery from saved IR. | +| Guards | Only demonstrated guards necessary for the selected operation. Request correlation must receive the actual request context. | Root tag, unique selected child and actual request ID/target checks are implemented. No universal expression AST is introduced. | + +The generated inherent method accepts only wire strings: + +```rust +spec.parse_response_with_request(&response, actual_request_id, actual_request_to)? +``` + +Use the ID and target of the stanza actually sent. The existing trait method +cannot supply that context and now returns `guards.request_context_required` +for correlated operations. It must not fabricate context from the response. +Unsupported reference paths fail admission explicitly. No transport, event or +consumer API is added, and no IR/schema changes are needed. + +## Verified pilots + +| Operation | Request evidence | Ordered outcomes | +| --- | --- | --- | +| SetSubject | `GROUP_JID(iqTo)`; `` content is `subjectElementValue`. | Success, client error, server error. | +| AcceptGroupAdd | `GROUP_JID(iqTo)`; `` reads `acceptCode`, `acceptExpiration`, `acceptAdmin`. | Success with `membership_approval_request`, bare success, client error, server error. | + +Both result and error mixins compare response `id`/`from` with request `id`/`to`. +SetSubject's empty success remains valid after envelope and context checks. +AcceptGroupAdd's client arm includes code 500 with text `resource-constraint`; +other code-500 errors can reach the later server arm. Classification therefore +uses ordered code/text pairs, not a guessed split between 4xx and 5xx. +The direct payload specialization requires matching IR field and error-arm +metadata, small bounded integer codes, and the demonstrated optional child shape. +Unsupported shapes retain explicit admission diagnostics. + +`WASmaxParseUtils.attrInt` uses decimal `parseInt`: `0304`, `+304`, `304tail` +and `304.9` decode to 304 before literal/range checks. The generated bounded +error decoder retains that behavior and ECMAScript leading whitespace. Required +`` is unique; duplicate approval children fail the gated success and +continue to bare success in source order. Unknown unrelated children remain +accepted. This is not general JavaScript evaluation or a universal guard AST. ### Source provenance @@ -97,6 +108,7 @@ was already present in the selected environment. `whatspec restore --from-lock - A: `65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55`. - B: `09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8`. - C: `34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2`. +- D: `7783a694b3a8bc3a379585817de0e477c75729c73b21adbffbf060f5f0ac2860`. Modules were extracted statically with `wa_transform::extract_module_definitions`. Offsets are byte intervals with exclusive ends. No `eval`, `vm`, sessions or @@ -115,6 +127,8 @@ credentials were used to extract or test protocol behavior. | `WASmaxInGroupsAcceptGroupAddResponseSuccess` | C | 5098..5459 | | `WASmaxOutGroupsAcceptGroupAddRequest` | C | 5461..5920 | | `WASmaxGroupsAcceptGroupAddRPC` | C | 5922..7533 | +| `WASmaxParseUtils` | B | 2256336..2263058 | +| `WASmaxInGroupsIQErrorResourceConstraintMixin` | D | 623..1153 | These are static observations of this preserved Web build, not proof of the server's complete contract or current account behavior. @@ -125,25 +139,27 @@ Four regression tests fail on the base commit and pass after the change: missing contract, wholly discarded payload, an actual unrepresentable nested response from `WAWebQueryBusinessCategoriesJob`, and ordered child-gated success. The empty-outcome control remains successful. The pilot test reads the real IQ IR. -The whole generated file is syntax-checked, not type-checked against an external -consumer. The conformance front owns independent execution validation. - -Two in-process debug generations of all 142 IQ operations produced identical -1,204,604-byte outputs, SHA-256 -`4a4473e33d9e8fbcdf87d47783b0ea6c654fdd13243695918910e72c380fb50f`, -in 2.43 and 2.40 seconds. 69 parsers have an explicit rejection diagnostic. -The base emitter produced 1,523,859 bytes in 2.84 and 2.98 seconds in the same -debug setup. The smaller output mostly removes parsers that could not preserve -all outcomes; it is not an optimization claim. These are local generation -measurements, not runtime or release-build benchmarks. -The reference Rust file is ignored by git; no new checked-in generated catalog or -runtime dependency is required. Future updates must review the rejected operation -set as well as source changes, without relaxing admission to preserve a count. - -Local validation passed `cargo test --locked --workspace`, including 176 -`wa-codegen` tests, -`cargo clippy --locked -p wa-codegen --all-targets -- -D warnings`, formatting, -and all 12 real JSON Schemas. Full bundle regeneration with `--check` reported -27 committed artifacts up to date. `scripts/lint-ir.py` still fails on the -pre-existing exact-count baselines; its output is byte-identical on the base -commit and this branch. This patch changes neither IR nor baselines. +The whole generated file is syntax-checked. A separate integration test compiles +and executes the two real generated pilots with a small in-memory node adapter: +requests, empty/gated successes, ordered nested error payloads, code/text ranges, +coercion, duplicate children, unknown children, and missing/mismatched request +context. Copying this test to the first fail-closed commit reproduces the missing +context API failure. The adapter does not qualify a binary codec or a downstream +client; independent conformance remains separately owned. + +Two debug generations of all 142 IQ operations produced identical 2,061,400-byte +outputs, SHA-256 `7443c11931dbfa69701838748101acf35982ebd1e4d8e22063aef4908632faf7`, +in 2.86 and 2.88 seconds, with 39 explicitly rejected parsers. The original base +produced 1,523,859 bytes in 2.84 and 2.98 seconds. The increase includes recovered +outcome types and context-taking parsers; it is not an optimization claim. +The Rust reference catalog remains ignored by git. Maintenance requires review +of the admitted/rejected operation set and the bounded helper against source +changes; neither a rejection count nor reduced output size is a quality target. + +Reproduce with `cargo test --locked -p wa-codegen`, including 178 unit tests and +the compiled runtime integration test; `cargo clippy --locked -p wa-codegen +--all-targets -- -D warnings`; schema validation; and full bundle regeneration +with `whatspec update --check`. No generated IR or baseline changes are included. +The separately owned dependency audit fix from PR #54 is carried as its own +commit. The six pre-existing exact-count lint baseline failures are outside +this change and are not suppressed. From 6267495ca9e068977f95903fab537640757dfe08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:07:44 -0300 Subject: [PATCH 10/52] fix: update rustls for RUSTSEC-2026-0285 Cherry-picked from 81d355cd7392872174201dee3617a480e7a9f5dd (PR #54). Preserves the wa-codegen 0.2.0 lock entry. --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a7ead95..3f21086a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1666,9 +1666,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -1689,9 +1689,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", From ebbfaedfdfab5543a796feadadae7ca397c45ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:08:33 +0000 Subject: [PATCH 11/52] test: bind conformance AST capture to verified bytes --- .../wa-mex/tests/independent_conformance.rs | 4 +- crates/wa-transform/examples/module_spans.rs | 10 ++++- tests/conformance/README.md | 20 ++++++--- tests/conformance/capture.py | 10 +++-- tests/conformance/test_capture.py | 41 +++++++++++++++++++ 5 files changed, 73 insertions(+), 12 deletions(-) diff --git a/crates/wa-mex/tests/independent_conformance.rs b/crates/wa-mex/tests/independent_conformance.rs index 299bbfec..26f2a2c6 100644 --- a/crates/wa-mex/tests/independent_conformance.rs +++ b/crates/wa-mex/tests/independent_conformance.rs @@ -29,8 +29,8 @@ fn verified_call_sites_keep_presence_separate_from_boolean_type() { op.variables_presence[key].presence, expected, "{version}: {key}" ); - // All four are used as Relay Condition keys. That does not establish - // whether a call-site value can be null or undefined. + // The scalar-name heuristic types these fetch_* names as boolean. + // That does not establish call-site nullability or key presence. assert_eq!(op.variables_shape[key], TypeNode::Leaf("boolean".into())); } assert_eq!( diff --git a/crates/wa-transform/examples/module_spans.rs b/crates/wa-transform/examples/module_spans.rs index a4676254..743bf477 100644 --- a/crates/wa-transform/examples/module_spans.rs +++ b/crates/wa-transform/examples/module_spans.rs @@ -1,6 +1,8 @@ //! Static module spans for reviewed conformance captures; never executes JS. //! Usage: module_spans BUNDLE MODULE [MODULE ...] +//! Use `-` for BUNDLE to parse verified bytes supplied on stdin. use anyhow::{Context, Result}; +use std::io::Read; fn main() -> Result<()> { let mut args = std::env::args().skip(1); @@ -9,7 +11,13 @@ fn main() -> Result<()> { .context("usage: module_spans BUNDLE MODULE ...")?; let names: Vec<_> = args.collect(); anyhow::ensure!(!names.is_empty(), "at least one module is required"); - let source = std::fs::read_to_string(path)?; + let source = if path == "-" { + let mut source = String::new(); + std::io::stdin().read_to_string(&mut source)?; + source + } else { + std::fs::read_to_string(path)? + }; for module in wa_transform::extract_module_definitions(&source) { if names.contains(&module.name) { println!("{}\t{}\t{}", module.name, module.start, module.end); diff --git a/tests/conformance/README.md b/tests/conformance/README.md index b711d62e..0025f866 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -75,7 +75,9 @@ undefined; `fetch_pinned_messages` remains undetermined because its external call is not analyzed. Nested input.type is always present while input.key and input.view_role can be omitted. -All these flag variables have inferred boolean type from Relay conditions. +All these flag variables have inferred boolean type from their `fetch_` names. +The extractor's scalar-name heuristic supplies that type; these tests do not +establish inference from Relay conditions. That type does not prove nullability or presence. Synthetic callers of the real GraphQL operation separately test null, false, undefined, a property read, a comparison and an unresolved call. Null survives JSON object-key serialization; @@ -93,9 +95,12 @@ python3 -m unittest discover -s tests/conformance -p 'test_*.py' cargo fmt --all -- --check ``` -The two Python tests check fixture span/hash identity and rejection of changed, +The Python tests check fixture span/hash identity and rejection of changed, missing, extra bundles, wrong setHash and wrong bundle count. They need no -network or compiled extractor. The Rust tests use only committed fixtures. +network or compiled extractor. Instrumented subprocess tests also cover input +mutation and single-quoted or escaped module names reaching the AST. Those tests +check the subprocess boundary, not AST correctness. The Rust tests use only +committed fixtures. The independent CI job `conformance-fixtures` runs `python3 tests/conformance/test_capture.py -v` on every workflow run, including when dependency checks fail in the separate `check` job. @@ -111,8 +116,10 @@ python3 tests/conformance/capture.py LOCK_JSON VERIFIED_BUNDLE_DIRECTORY NEW_CAP `target/debug/examples/module_spans`. If using a custom Cargo target directory, copy that executable there or build the example with the default target first. Review all changed definitions and dependency paths before changing expected -outcomes. The raw substring prefilter only selects candidate bundle files; -AST module definitions supply the actual byte spans. +outcomes. Every bundle is parsed without a substring prefilter, so valid string +spellings are not silently excluded. The helper receives the already verified +bytes on stdin; it does not reread the input file. AST module definitions supply +the actual byte spans. The 2026-10-07 cloud executor's `whatspec restore` failed with `invalid peer certificate: UnknownIssuer`. Downloading the *same* release asset @@ -144,6 +151,9 @@ All 13 provenance locations changed. Updating this selected baseline required zero semantic expectation changes and zero source overrides. The two fixture sets, including provenance, occupy 16,901 and 16,905 bytes. This observation does not establish stability of the rest of either bundle or eliminate future review. +Recapture with verified bytes on stdin and no lexical prefilter reproduced both +sets byte-for-byte. It took 23.265 s for the old snapshot and 18.812 s for the +current snapshot on this executor, excluding download and compilation. Generation used baseline HEAD, the release-profile CLI, rustc 1.99.0, x86_64 Linux, offline restored bundles, and separate initially empty output directories. diff --git a/tests/conformance/capture.py b/tests/conformance/capture.py index 0dbc165a..976824ce 100644 --- a/tests/conformance/capture.py +++ b/tests/conformance/capture.py @@ -45,10 +45,12 @@ def capture(lock_path, bundles, output): executable = Path(__file__).resolve().parents[2] / 'target/debug/examples/module_spans' captured = {} evidence = [] - for path, data in raw: - if not any(('"' + name + '"').encode() in data for name in MODULES): - continue - spans = subprocess.check_output([str(executable), str(path), *MODULES], text=True) + for _, data in raw: + # Parse precisely the bytes verified above. A path reread can race with + # changes to the input files. Let the AST handle all string spellings. + spans = subprocess.check_output( + [str(executable), '-', *MODULES], input=data + ).decode('utf-8') for row in spans.splitlines(): name, start, end = row.split('\t') start, end = int(start), int(end) diff --git a/tests/conformance/test_capture.py b/tests/conformance/test_capture.py index a668c330..59d97711 100644 --- a/tests/conformance/test_capture.py +++ b/tests/conformance/test_capture.py @@ -4,11 +4,52 @@ from pathlib import Path import tempfile import unittest +from unittest.mock import patch from capture import MODULES, capture class CaptureTests(unittest.TestCase): + def capture_synthetic(self, spelling, mutate_file=False): + name = 'ConformanceModule' + source = f'__d({spelling},[],function(){{}});'.encode() + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + bundles = root / 'bundles' + bundles.mkdir() + bundle = bundles / 'input.js' + bundle.write_bytes(source) + digest = hashlib.sha256(source).hexdigest() + lock = dict(waVersion='fixture', bundleCount=1, + setHash=hashlib.sha256(f'{digest}:{len(source)}'.encode()).hexdigest(), + bundles=[dict(sha256=digest, size=len(source))]) + lock_path = root / 'lock.json' + lock_path.write_text(json.dumps(lock)) + + def ast(args, **kwargs): + # An instrumented subprocess boundary, not a replacement AST + # oracle: prove the verified bytes are sent without rereading. + if mutate_file: + bundle.write_bytes(b'/* moved offsets */' + source) + self.assertEqual(args[1], '-') + self.assertEqual(kwargs.get('input'), source) + return f'{name}\t0\t{len(source) - 1}\n'.encode() + + with patch('capture.MODULES', [name]), patch( + 'capture.subprocess.check_output', side_effect=ast + ) as parser: + capture(lock_path, bundles, root / 'output') + parser.assert_called_once() + self.assertEqual((root / 'output' / f'{name}.js').read_bytes(), source + b'\n') + + def test_ast_receives_the_verified_bytes_even_if_original_file_changes(self): + self.capture_synthetic('"ConformanceModule"', mutate_file=True) + + def test_single_quoted_and_escaped_names_reach_the_ast(self): + for spelling in ["'ConformanceModule'", r"'\u0043onformanceModule'"]: + with self.subTest(spelling=spelling): + self.capture_synthetic(spelling) + def test_committed_sources_match_every_recorded_span_hash(self): for version in ['2.3000.1045368834', '2.3000.1047483476']: root = Path(__file__).parent / version From a79c93f3fef9f15778cda88d889113011ea7d5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:17:14 -0300 Subject: [PATCH 12/52] test(codegen): document source-ordered malformed error fallback --- crates/wa-codegen/src/spec.rs | 3 ++ .../wa-codegen/tests/fixtures/iq_runtime.rs | 41 +++++++++++++++++++ docs/iq-response-admission.md | 8 +++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/wa-codegen/src/spec.rs b/crates/wa-codegen/src/spec.rs index fdb98775..e76c496e 100644 --- a/crates/wa-codegen/src/spec.rs +++ b/crates/wa-codegen/src/spec.rs @@ -481,6 +481,9 @@ fn emit_direct_error_parser( &struct_name, )); lines.push(format!("{indent} }})();")); + // The source disjunction tries the next parser after *any* failure, + // including a malformed specific payload. Do not commit on code/text: + // SetSubject 406/not-acceptable can still reach the 400..499 fallback. lines.push(format!("{indent} if let Ok(mut value) = parsed {{ value.{} = code as {}; return Ok({ename}::{vname}(value)); }}", rust_ident(&code_field.name), crate::fields::integer_width(code_field))); lines.push(format!("{indent} }}")); } diff --git a/crates/wa-codegen/tests/fixtures/iq_runtime.rs b/crates/wa-codegen/tests/fixtures/iq_runtime.rs index 5e255153..5b67f418 100644 --- a/crates/wa-codegen/tests/fixtures/iq_runtime.rs +++ b/crates/wa-codegen/tests/fixtures/iq_runtime.rs @@ -300,6 +300,47 @@ fn main() { .unwrap(), MakeAcceptGroupAddRequestResponse::Success(_) )); + // WASmaxInGroupsSetSubjectClientErrors continues after any specific parser + // failure, including malformed/duplicate optional , into 400..499. + let valid_field = Node::new( + "field", + &[("name", "subject"), ("reason", "invalid")], + vec![], + ); + for (fields, specific) in [ + (vec![], true), + (vec![valid_field.clone()], true), + ( + vec![Node::new("field", &[("name", "subject")], vec![])], + false, + ), + (vec![valid_field.clone(), valid_field], false), + ] { + let mut node = error("406", Some("not-acceptable")); + node.children[0].children = fields; + let MakeSetSubjectRequestResponse::ClientError(value) = subject + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap() + else { + panic!("expected client error"); + }; + match value.error_set_subject_client_errors.unwrap() { + MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorNotAcceptable( + value, + ) => { + assert!(specific); + assert_eq!(value.code, 406); + } + MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorFallbackClient( + value, + ) => { + assert!(!specific); + assert_eq!(value.code, 406); + assert_eq!(value.text, "not-acceptable"); + } + _ => panic!("unexpected error arm"), + } + } for wire in ["id", "from"] { let mut invalid = bare.clone(); invalid.attrs.remove(wire); diff --git a/docs/iq-response-admission.md b/docs/iq-response-admission.md index 7e2443b4..182ea9fb 100644 --- a/docs/iq-response-admission.md +++ b/docs/iq-response-admission.md @@ -90,8 +90,11 @@ Unsupported shapes retain explicit admission diagnostics. and `304.9` decode to 304 before literal/range checks. The generated bounded error decoder retains that behavior and ECMAScript leading whitespace. Required `` is unique; duplicate approval children fail the gated success and -continue to bare success in source order. Unknown unrelated children remain -accepted. This is not general JavaScript evaluation or a universal guard AST. +continue to bare success in source order. For SetSubject 406/`not-acceptable`, a malformed or duplicated optional +`` makes the specific parser fail; the source disjunction then tries the +400–499 fallback, which succeeds. Code/text selection is not a commit point. +The compiled regression checks absent, valid, malformed and duplicated children. +Unknown unrelated children remain accepted. This is not general JavaScript evaluation or a universal guard AST. ### Source provenance @@ -119,6 +122,7 @@ credentials were used to extract or test protocol behavior. | `WASmaxOutGroupsSetSubjectChangeSubjectMixin` | A | 27847..28186 | | `WASmaxOutGroupsSetSubjectRequest` | A | 28188..28588 | | `WASmaxGroupsSetSubjectRPC` | A | 28590..29826 | +| `WASmaxInGroupsSetSubjectClientErrors` | A | 24289..26417 | | `WASmaxOutGroupsBaseIQSetRequestMixin` | B | 1138342..1138644 | | `WASmaxOutGroupsBaseSetGroupMixin` | B | 1138646..1139064 | | `WASmaxInGroupsIQResultResponseMixin` | B | 107120..107921 | From a863bfb517ce2ba382c2d7990c88070f642655bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:27:16 +0000 Subject: [PATCH 13/52] test: expose binary error-content divergence in composed IQ pilots --- .../tests/fixtures/independent_iq_cases.rs | 292 +++ crates/wa-codegen/tests/independent_iq.rs | 93 + tests/conformance/compiled/README.md | 136 ++ tests/conformance/compiled/capture_errors.py | 35 + .../compiled/inputs/2.3000.1045368834.json | 1674 +++++++++++++++++ .../compiled/inputs/2.3000.1047483476.json | 1674 +++++++++++++++++ .../compiled/inputs/provenance.json | 16 + ...ASmaxInGroupsAcceptGroupAddClientErrors.js | 1 + ...GroupsAcceptGroupAddResponseClientError.js | 1 + ...GroupsAcceptGroupAddResponseServerError.js | 1 + ...WASmaxInGroupsIQErrorAlreadyExistsMixin.js | 1 + .../WASmaxInGroupsIQErrorBadRequestMixin.js | 1 + ...ASmaxInGroupsIQErrorFallbackClientMixin.js | 1 + ...ASmaxInGroupsIQErrorFallbackServerMixin.js | 1 + ...InGroupsIQErrorInternalServerErrorMixin.js | 1 + ...WASmaxInGroupsIQErrorNotAcceptableMixin.js | 1 + ...xInGroupsIQErrorPartialServerErrorMixin.js | 1 + ...xInGroupsIQErrorResourceConstraintMixin.js | 1 + .../WASmaxInGroupsIQErrorResponseMixin.js | 1 + ...xInGroupsIQErrorServiceUnavailableMixin.js | 1 + .../WASmaxInGroupsServerErrors.js | 1 + .../WASmaxInGroupsSetSubjectClientErrors.js | 1 + ...axInGroupsSetSubjectResponseClientError.js | 1 + ...axInGroupsSetSubjectResponseServerError.js | 1 + .../2.3000.1045368834/WASmaxParseReference.js | 1 + .../2.3000.1045368834/WASmaxParseUtils.js | 1 + .../sources/2.3000.1045368834/provenance.json | 141 ++ ...ASmaxInGroupsAcceptGroupAddClientErrors.js | 1 + ...GroupsAcceptGroupAddResponseClientError.js | 1 + ...GroupsAcceptGroupAddResponseServerError.js | 1 + ...WASmaxInGroupsIQErrorAlreadyExistsMixin.js | 1 + .../WASmaxInGroupsIQErrorBadRequestMixin.js | 1 + ...ASmaxInGroupsIQErrorFallbackClientMixin.js | 1 + ...ASmaxInGroupsIQErrorFallbackServerMixin.js | 1 + ...InGroupsIQErrorInternalServerErrorMixin.js | 1 + ...WASmaxInGroupsIQErrorNotAcceptableMixin.js | 1 + ...xInGroupsIQErrorPartialServerErrorMixin.js | 1 + ...xInGroupsIQErrorResourceConstraintMixin.js | 1 + .../WASmaxInGroupsIQErrorResponseMixin.js | 1 + ...xInGroupsIQErrorServiceUnavailableMixin.js | 1 + .../WASmaxInGroupsServerErrors.js | 1 + .../WASmaxInGroupsSetSubjectClientErrors.js | 1 + ...axInGroupsSetSubjectResponseClientError.js | 1 + ...axInGroupsSetSubjectResponseServerError.js | 1 + .../2.3000.1047483476/WASmaxParseReference.js | 1 + .../2.3000.1047483476/WASmaxParseUtils.js | 1 + .../sources/2.3000.1047483476/provenance.json | 141 ++ 47 files changed, 4240 insertions(+) create mode 100644 crates/wa-codegen/tests/fixtures/independent_iq_cases.rs create mode 100644 crates/wa-codegen/tests/independent_iq.rs create mode 100644 tests/conformance/compiled/README.md create mode 100644 tests/conformance/compiled/capture_errors.py create mode 100644 tests/conformance/compiled/inputs/2.3000.1045368834.json create mode 100644 tests/conformance/compiled/inputs/2.3000.1047483476.json create mode 100644 tests/conformance/compiled/inputs/provenance.json create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddClientErrors.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseClientError.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseServerError.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorAlreadyExistsMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorBadRequestMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorFallbackClientMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorFallbackServerMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorInternalServerErrorMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorNotAcceptableMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorPartialServerErrorMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorResourceConstraintMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorResponseMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorServiceUnavailableMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsServerErrors.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectClientErrors.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseClientError.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseServerError.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxParseReference.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxParseUtils.js create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/provenance.json create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddClientErrors.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseClientError.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseServerError.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorAlreadyExistsMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorBadRequestMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorFallbackClientMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorFallbackServerMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorInternalServerErrorMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorNotAcceptableMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorPartialServerErrorMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorResourceConstraintMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorResponseMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorServiceUnavailableMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsServerErrors.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectClientErrors.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseClientError.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseServerError.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxParseReference.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxParseUtils.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/provenance.json diff --git a/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs b/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs new file mode 100644 index 00000000..ae41b9c5 --- /dev/null +++ b/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs @@ -0,0 +1,292 @@ +// Expectations are manually read from preserved Web module bodies. No expected +// result is generated from IQ IR, codegen output or the other pilot's test main. +fn specs() -> (MakeSetSubjectRequestSpec, MakeAcceptGroupAddRequestSpec) { + let target = jid::Jid::new("123", jid::Server::Group); + ( + MakeSetSubjectRequestSpec::new(target.clone(), "Olá & ".as_bytes().to_vec()), + MakeAcceptGroupAddRequestSpec::new( + "invite-42", + 98765, + &jid::Jid::new("456", jid::Server::Pn), + target, + ), + ) +} +fn subject_class(node: &Node) -> Result<&'static str, anyhow::Error> { + Ok( + match specs() + .0 + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us")? + { + MakeSetSubjectRequestResponse::Success(_) => "success", + MakeSetSubjectRequestResponse::ClientError(_) => "client", + MakeSetSubjectRequestResponse::ServerError(_) => "server", + }, + ) +} +fn accept_class(node: &Node) -> Result<&'static str, anyhow::Error> { + Ok( + match specs() + .1 + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us")? + { + MakeAcceptGroupAddRequestResponse::Success(_) => "success", + MakeAcceptGroupAddRequestResponse::GroupJoinRequestSuccess(_) => "approval", + MakeAcceptGroupAddRequestResponse::ClientError(_) => "client", + MakeAcceptGroupAddRequestResponse::ServerError(_) => "server", + }, + ) +} +#[test] +fn request_destination_and_content() { + let (subject, accept) = specs(); + let request = subject.build_iq(); + assert_eq!(request.target.0, "123@g.us"); + assert_eq!((request.namespace, request.kind), ("w:g2", "set")); + let node::NodeContent::Nodes(nodes) = request.content.unwrap(); + assert_eq!(nodes.len(), 1); + assert_eq!(nodes[0].tag, "subject"); + assert_eq!(nodes[0].bytes.as_deref(), Some("Olá & ".as_bytes())); + let request = accept.build_iq(); + assert_eq!(request.target.0, "123@g.us"); + assert_eq!((request.namespace, request.kind), ("w:g2", "set")); + let node::NodeContent::Nodes(nodes) = request.content.unwrap(); + assert_eq!(nodes.len(), 1); + assert_eq!(nodes[0].tag, "accept"); + assert_eq!( + nodes[0].attrs, + BTreeMap::from([ + ("code".into(), "invite-42".into()), + ("expiration".into(), "98765".into()), + ("admin".into(), "456@s.whatsapp.net".into()) + ]) + ); +} +#[test] +fn success_order_and_runtime_extensions() { + let approval = Node::new("membership_approval_request", &[], vec![]); + for (children, expected) in [ + (vec![], "success"), + (vec![approval.clone()], "approval"), + (vec![approval.clone(), approval], "success"), + (vec![Node::new("future", &[], vec![])], "success"), + ] { + let node = result(children); + assert_eq!(subject_class(&node).unwrap(), "success"); + assert_eq!(accept_class(&node).unwrap(), expected); + } + // Bare success does not examine children or content. A failed approval + // selector is followed by bare success, even with binary root content. + let mut node = result(vec![]); + node.bytes = Some(vec![0xff]); + assert_eq!(accept_class(&node).unwrap(), "success"); + assert_eq!(subject_class(&node).unwrap(), "success"); +} +#[test] +fn ordered_error_outcomes_and_ranges() { + for (code, text, accept, subject) in [ + ("304", "already-exists", Some("client"), None), + ("304", "bad-request", None, None), + ("399", "unknown", None, None), + ("400", "bad-request", Some("client"), Some("client")), + ("400", "unknown", Some("client"), Some("client")), + ("499", "unknown", Some("client"), Some("client")), + ("500", "resource-constraint", Some("client"), Some("server")), + ( + "500", + "internal-server-error", + Some("server"), + Some("server"), + ), + ("500", "unknown", Some("server"), Some("server")), + ("503", "service-unavailable", Some("server"), Some("server")), + ( + "530", + "partial-server-error", + Some("server"), + Some("server"), + ), + ("599", "unknown", Some("server"), Some("server")), + ("600", "unknown", None, None), + ] { + let node = error(code, Some(text)); + assert_eq!(accept_class(&node).ok(), accept, "accept: {code}/{text}"); + assert_eq!(subject_class(&node).ok(), subject, "subject: {code}/{text}"); + } +} +#[test] +fn decimal_coercion_and_invalid_codes() { + for code in ["0304", "+304", "\t304tail", "\u{feff}304", "304.9", "304e7"] { + assert_eq!( + accept_class(&error(code, Some("already-exists"))).unwrap(), + "client" + ); + } + for code in [ + "", + "+", + "-304", + "0x130", + "\u{0085}304", + "∞", + "NaN", + "9999999999999999999999", + " + 304", + ] { + assert!( + accept_class(&error(code, Some("already-exists"))).is_err(), + "{code:?}" + ); + } + for key in ["code", "text"] { + let mut node = error("400", Some("bad-request")); + node.children[0].attrs.remove(key); + assert!(accept_class(&node).is_err()); + assert!(subject_class(&node).is_err()); + } + let mut node = error("400", Some("bad-request")); + node.children.push(node.children[0].clone()); + assert!(accept_class(&node).is_err()); + assert!(subject_class(&node).is_err()); +} +#[test] +fn correlation_is_required_for_every_response_kind() { + let (subject, accept) = specs(); + for node in [ + result(vec![]), + result(vec![Node::new("membership_approval_request", &[], vec![])]), + error("400", Some("bad-request")), + error("500", Some("internal-server-error")), + ] { + assert!(subject.parse_response(&node.as_ref()).is_err()); + assert!(accept.parse_response(&node.as_ref()).is_err()); + for key in ["id", "from", "type"] { + let mut invalid = node.clone(); + invalid.attrs.remove(key); + assert!(subject_class(&invalid).is_err()); + assert!(accept_class(&invalid).is_err()); + invalid.attrs.insert(key.into(), "wrong".into()); + assert!(subject_class(&invalid).is_err()); + assert!(accept_class(&invalid).is_err()); + } + let mut invalid = node.clone(); + invalid.tag = "message".into(); + assert!(subject_class(&invalid).is_err()); + assert!(accept_class(&invalid).is_err()); + for (id, to) in [ + ("wrong", "123@g.us"), + ("req-1", "999@g.us"), + ("", "123@g.us"), + ("req-1", ""), + ] { + assert!( + subject + .parse_response_with_request(&node.as_ref(), id, to) + .is_err() + ); + assert!( + accept + .parse_response_with_request(&node.as_ref(), id, to) + .is_err() + ); + } + } + // The actual wire request context controls correlation, not the spec's + // original target and not values fabricated from the received stanza. + let mut node = result(vec![]); + node.attrs.insert("from".into(), "999@g.us".into()); + assert!( + subject + .parse_response_with_request(&node.as_ref(), "req-1", "999@g.us") + .is_ok() + ); + assert!( + accept + .parse_response_with_request(&node.as_ref(), "req-1", "999@g.us") + .is_ok() + ); +} +#[test] +fn optional_field_payload_and_source_fallback() { + let subject = specs().0; + let mut node = error("406", Some("not-acceptable")); + node.children[0].children = vec![Node::new( + "field", + &[("name", "subject"), ("reason", "too-long")], + vec![], + )]; + let MakeSetSubjectRequestResponse::ClientError(value) = subject + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap() + else { + panic!("client error required") + }; + let Some(MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorNotAcceptable( + payload, + )) = value.error_set_subject_client_errors + else { + panic!("specific error required") + }; + assert_eq!( + ( + payload.code, + payload.text.as_str(), + payload.name.as_str(), + payload.reason.as_str() + ), + (406, "not-acceptable", "subject", "too-long") + ); + for children in [ + vec![Node::new("field", &[("name", "subject")], vec![])], + vec![ + Node::new("field", &[], vec![]), + Node::new("field", &[], vec![]), + ], + ] { + let mut node = error("406", Some("not-acceptable")); + node.children[0].children = children; + let MakeSetSubjectRequestResponse::ClientError(value) = subject + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap() + else { + panic!("client error required") + }; + let Some( + MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorFallbackClient( + payload, + ), + ) = value.error_set_subject_client_errors + else { + panic!("source disjunction must fall through") + }; + assert_eq!( + (payload.code, payload.text.as_str()), + (406, "not-acceptable") + ); + } +} +#[test] +fn binary_error_content_reaches_fallback() { + let subject = specs().0; + let mut response = error("406", Some("not-acceptable")); + response.children[0].bytes = Some(b"opaque".to_vec()); + // The optionalChildWithTag path rejects Uint8Array before code/text. + // SetSubjectClientErrors then tries the 400..499 fallback, which succeeds. + let MakeSetSubjectRequestResponse::ClientError(value) = subject + .parse_response_with_request(&response.as_ref(), "req-1", "123@g.us") + .unwrap() + else { + panic!("client error required") + }; + assert!( + matches!( + value.error_set_subject_client_errors, + Some( + MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorFallbackClient( + _ + ) + ) + ), + "binary error content must reach fallback: {value:?}" + ); +} diff --git a/crates/wa-codegen/tests/independent_iq.rs b/crates/wa-codegen/tests/independent_iq.rs new file mode 100644 index 00000000..3099f976 --- /dev/null +++ b/crates/wa-codegen/tests/independent_iq.rs @@ -0,0 +1,93 @@ +//! Independent cases read from verified Web module bodies, executed against +//! freshly emitted Rust. Requires the explicit #51 + #53 composition. +use std::{fs, process::Command}; + +fn qualify(label: &str, input: &str) { + let mut ir: wa_ir::IqIr = serde_json::from_str(input).unwrap(); + ir.stanzas.retain(|op| { + matches!( + op.module_name.as_str(), + "WASmaxOutGroupsSetSubjectRequest" | "WASmaxOutGroupsAcceptGroupAddRequest" + ) + }); + assert_eq!(ir.stanzas.len(), 2); + let dir = std::env::temp_dir().join(format!( + "whatspec-independent-iq-{label}-{}", + std::process::id() + )); + fs::create_dir_all(&dir).unwrap(); + let output = wa_codegen::generate_iq(&ir); + assert_eq!(output, wa_codegen::generate_iq(&ir)); + fs::write(dir.join("generated.rs"), output).unwrap(); + // Reuse only the reference-emitter adapter, imports and tree constructors. + // None of the other workstream's assertions or expectations are executed. + let adapter = include_str!("fixtures/iq_runtime.rs") + .split_once("\nfn main() {") + .expect("explicit adapter/main boundary from #53") + .0; + fs::write( + dir.join("main.rs"), + format!( + "{adapter}\n{}", + include_str!("fixtures/independent_iq_cases.rs") + ), + ) + .unwrap(); + let deps = std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .to_path_buf(); + let anyhow = fs::read_dir(&deps) + .unwrap() + .map(|e| e.unwrap().path()) + .find(|p| { + p.file_name() + .unwrap() + .to_string_lossy() + .starts_with("libanyhow-") + && p.extension().is_some_and(|e| e == "rlib") + }) + .unwrap(); + let compile = Command::new("rustc") + .arg("--edition=2024") + .arg("--test") + .arg(dir.join("main.rs")) + .arg("--extern") + .arg(format!("anyhow={}", anyhow.display())) + .arg("-L") + .arg(format!("dependency={}", deps.display())) + .arg("-o") + .arg(dir.join("run")) + .output() + .unwrap(); + assert!( + compile.status.success(), + "{}", + String::from_utf8_lossy(&compile.stderr) + ); + let run = Command::new(dir.join("run")).output().unwrap(); + assert!( + run.status.success(), + "runtime cases failed; generated source retained at {}:\n{}\n{}", + dir.display(), + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + fs::remove_dir_all(dir).unwrap(); +} + +#[test] +fn current_snapshot_generated_code() { + qualify( + "current", + include_str!("../../../tests/conformance/compiled/inputs/2.3000.1047483476.json"), + ); +} +#[test] +fn previous_snapshot_generated_code() { + qualify( + "previous", + include_str!("../../../tests/conformance/compiled/inputs/2.3000.1045368834.json"), + ); +} diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md new file mode 100644 index 00000000..1ad94a44 --- /dev/null +++ b/tests/conformance/compiled/README.md @@ -0,0 +1,136 @@ +# Compiled qualification of #51 and #53 + +Status: **blocked by a reproduced source/output divergence**. This test is meant +for an explicitly stacked qualification change after the IQ workstream addresses +the finding. Do not silently add it to #51 alone: it requires #53's generated +context-taking API and small runtime adapter. + +The local `test/composed-iq-qualification` branch composes these immutable heads: + +- #51: `ebbfaedfdfab5543a796feadadae7ca397c45ff9`. +- #53: `a79c93f3fef9f15778cda88d889113011ea7d5a1`. +- Local composition commit: `1f847fe591057670986ea6cac82352f0c9144c27`. + +No published head was changed. No emitter or other IQ-workstream file was edited. +The local merge preserved `wa-codegen` 0.2.0 and the exact audit dependency +versions from #54. + +## Method and inputs + +`crates/wa-codegen/tests/independent_iq.rs` runs `generate_iq` twice for each +snapshot and checks byte identity. It compiles each resulting Rust source with +`rustc --test`, then executes seven independently authored test groups in +`fixtures/independent_iq_cases.rs`. The expected outcomes are not generated from +IR or codegen. Only the in-memory node adapter, imports and constructors are +reused from #53; none of its expected-case assertions or main are run. + +The selected input JSON files contain the two actual stanza records from the +committed IQ IR at `1a441f0` and `1f5167c`. Their full-source and selected-file +hashes are in `inputs/provenance.json`. Both inputs validate against the real IQ +schema. Metadata version is retained and unrelated unparseable entries removed. +These files are generator inputs, never sources of expected outcomes. + +The pre-existing #51 captures establish requests and successful responses. +`sources/` adds 19 exact modules covering error envelopes, selected +code/text parsers, error disjunctions, reference access and parsing helpers. +Both complete bundle sets were verified again before AST capture. Each added +module has a hash and byte span in `provenance.json`. All 19 module bodies are +identical across the two snapshots. No JavaScript was executed. + +To recapture with the same verified flow, build the `module_spans` example in +the default target directory, then run: + +```sh +python3 tests/conformance/compiled/capture_errors.py LOCK BUNDLES NEW_OUTPUT +``` + +## Executed results + +For **each** WA snapshot, 2.3000.1045368834 and 2.3000.1047483476: + +| Compiled test group | Result | +| --- | --- | +| Request destination, namespace, type, subject bytes and accept attributes | Pass | +| Empty success, approval success, duplicate-child fallback, unknown extension | Pass | +| Ordered client/server outcomes, 304 exception, 400–499 and 500–599 bounds | Pass | +| Decimal coercion and malformed/absent/duplicate error inputs | Pass | +| Correlation for every response kind, absent/mismatched attrs, actual request context | Pass | +| Optional field payload and fallback after malformed/duplicate field | Pass | +| Binary error content before optional-child parsing | **Fail** | + +The two pilot outputs are each 63,757 bytes. Their SHA-256 values are: + +- Old: `2250a8ec0ea5ddf7a06250cc9be5ca74056012ae25a24aeb4872c5e0088d5763`. +- Current: `bbfcb27f75341e5d6bda2d98fcb1aaa1357864159167cb2e3fbf33377097ff84`. + +The difference is the snapshot version in the generated header. The two-snapshot +compile-and-run test phase completed in about 0.82 seconds after building its +Rust test driver. This is a single shared-executor observation, not a benchmark. + +## Minimal finding for the IQ workstream + +Use a correlated SetSubject response with this tree: + +```text +iq {type: "error", id: "req-1", from: "123@g.us"} + error {code: "406", text: "not-acceptable"} + content: binary bytes "opaque" +``` + +The source result is client error **IQErrorFallbackClient**. The compiled output +returns **IQErrorNotAcceptable**, with default empty `name` and `reason` strings. + +Source trace: + +1. `WASmaxInGroupsIQErrorNotAcceptableMixin` calls + `optionalChildWithTag(error, "field", parser)` before checking code/text. +2. `WASmaxParseUtils.optionalChildWithTag` calls `optionalChild`, which calls + `maybeChildren`. `maybeChildren` rejects `content instanceof Uint8Array`. +3. `WASmaxInGroupsSetSubjectClientErrors` continues after any specific failure. +4. `WASmaxInGroupsIQErrorFallbackClientMixin` reads only text and code in + 400..499, so this input reaches that fallback successfully. + +The generated selector checks `get_children_by_tag("field").count() <= 1`, +then treats a missing optional child as default empty fields. It does not +establish that the error content is a child list or absent. Both snapshots +reproduce the same mismatch. The test sets only `bytes`, with an empty children +vector, so it does not create an impossible mixed binary/children input. + +For current snapshot bundle +`09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8`: + +- `WASmaxParseUtils`: bytes 2256336..2263058. +- `WASmaxInGroupsIQErrorNotAcceptableMixin`: bytes 88701..89641. +- `WASmaxInGroupsIQErrorFallbackClientMixin`: bytes 85725..86174. + +The source disjunction is in bundle +`65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55`, +`WASmaxInGroupsSetSubjectClientErrors`, bytes 24289..26417. +All ends are exclusive. Hashes/spans for the old snapshot are retained alongside +its captured sources. + +Reproduce the full matrix: + +```sh +cargo test --locked -p wa-codegen --test independent_iq +``` + +On failure the generated source and compiled executable are retained in the +reported temporary directory. Run that executable with +`--exact binary_error_content_reaches_fallback` for the minimal failing case. + +## Limits and next step + +This executes generated Rust, not the Web client or server. The adapter is a +small tree implementation satisfying the reference emitter's existing paths; +it is not an external client's implementation and adds no client dependency. +It does not qualify binary encoding, arbitrary JS attribute types, JID helpers, +all builder-input rejection, transport, or the other IQ operations. The success +and error expectations are static source observations of preserved builds. + +The IQ owner should choose a faithful content-kind guard or an explicit +unsupported-contract diagnosis when that semantic check cannot be expressed. +No change to their emitter is made here. After their fix, rerun this same failing +case and the complete matrix before proposing a stacked qualification PR with +explicit dependencies on both #51 and #53. Do not weaken the expected fallback +or turn the failure into an ignored test. diff --git a/tests/conformance/compiled/capture_errors.py b/tests/conformance/compiled/capture_errors.py new file mode 100644 index 00000000..5426f553 --- /dev/null +++ b/tests/conformance/compiled/capture_errors.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Capture the reviewed error-parser evidence with the shared verified AST flow. + +Usage: python3 tests/conformance/compiled/capture_errors.py LOCK BUNDLES OUTPUT +""" +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +import capture + +capture.MODULES = [ + 'WASmaxInGroupsIQErrorResponseMixin', + 'WASmaxInGroupsSetSubjectClientErrors', + 'WASmaxInGroupsAcceptGroupAddClientErrors', + 'WASmaxInGroupsServerErrors', + 'WASmaxInGroupsSetSubjectResponseClientError', + 'WASmaxInGroupsAcceptGroupAddResponseClientError', + 'WASmaxInGroupsSetSubjectResponseServerError', + 'WASmaxInGroupsAcceptGroupAddResponseServerError', + 'WASmaxParseUtils', + 'WASmaxParseReference', + 'WASmaxInGroupsIQErrorNotAcceptableMixin', + 'WASmaxInGroupsIQErrorResourceConstraintMixin', + 'WASmaxInGroupsIQErrorFallbackClientMixin', + 'WASmaxInGroupsIQErrorFallbackServerMixin', + 'WASmaxInGroupsIQErrorAlreadyExistsMixin', + 'WASmaxInGroupsIQErrorBadRequestMixin', + 'WASmaxInGroupsIQErrorInternalServerErrorMixin', + 'WASmaxInGroupsIQErrorServiceUnavailableMixin', + 'WASmaxInGroupsIQErrorPartialServerErrorMixin', +] + +if __name__ == '__main__': + capture.capture(*(Path(arg) for arg in sys.argv[1:])) diff --git a/tests/conformance/compiled/inputs/2.3000.1045368834.json b/tests/conformance/compiled/inputs/2.3000.1045368834.json new file mode 100644 index 00000000..2a24df51 --- /dev/null +++ b/tests/conformance/compiled/inputs/2.3000.1045368834.json @@ -0,0 +1,1674 @@ +{ + "schemaVersion": "4.3.0", + "waVersion": "2.3000.1045368834", + "stanzas": [ + { + "moduleName": "WASmaxOutGroupsAcceptGroupAddRequest", + "namespace": "w:g2", + "iqType": "set", + "target": "group_jid", + "parserName": "WASmaxGroupsAcceptGroupAddRPC", + "exportedFunction": "makeAcceptGroupAddRequest", + "allExports": [ + "makeAcceptGroupAddRequest" + ], + "request": { + "namespace": "w:g2", + "iqType": "set", + "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], + "children": [ + { + "tag": "accept", + "attrs": [ + { + "name": "code", + "kind": "string", + "required": true, + "argPath": [ + { + "key": "acceptCode" + } + ] + }, + { + "name": "expiration", + "kind": "integer", + "required": true, + "argPath": [ + { + "key": "acceptExpiration" + } + ] + }, + { + "name": "admin", + "kind": "user_jid", + "required": true, + "argPath": [ + { + "key": "acceptAdmin" + } + ] + } + ], + "children": [], + "repeats": false + } + ] + }, + "response": { + "parserName": "WASmaxGroupsAcceptGroupAddRPC", + "assertions": [], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ], + "variants": [ + { + "tag": "AcceptGroupAddResponseGroupJoinRequestSuccess", + "moduleName": "WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess", + "kind": "success", + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "result" + }, + { + "kind": "child", + "name": "membership_approval_request" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ] + }, + { + "tag": "AcceptGroupAddResponseSuccess", + "moduleName": "WASmaxInGroupsAcceptGroupAddResponseSuccess", + "kind": "success", + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "result" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ] + }, + { + "tag": "AcceptGroupAddResponseClientError", + "moduleName": "WASmaxInGroupsAcceptGroupAddResponseClientError", + "kind": "error", + "errorArms": [ + { + "name": "IQErrorAlreadyExists", + "code": 304, + "text": "already-exists" + }, + { + "name": "IQErrorBadRequest", + "code": 400, + "text": "bad-request" + }, + { + "name": "IQErrorNotAuthorized", + "code": 401, + "text": "not-authorized" + }, + { + "name": "IQErrorForbidden", + "code": 403, + "text": "forbidden" + }, + { + "name": "IQErrorItemNotFound", + "code": 404, + "text": "item-not-found" + }, + { + "name": "IQErrorConflict", + "code": 409, + "text": "conflict" + }, + { + "name": "IQErrorGone", + "code": 410, + "text": "gone" + }, + { + "name": "IQErrorResourceLimit", + "code": 419, + "text": "resource-limit" + }, + { + "name": "IQErrorLocked", + "code": 423, + "text": "locked" + }, + { + "name": "IQErrorResourceConstraint", + "code": 500, + "text": "resource-constraint" + }, + { + "name": "IQErrorNotAllowed", + "code": 405, + "text": "not-allowed" + }, + { + "name": "IQErrorParentLinkedGroupsParticipantsResourceLimit", + "code": 412, + "text": "linked-groups-participants-resource-limit" + }, + { + "name": "IQErrorFallbackClient", + "codeMin": 400, + "codeMax": 499 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "error" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "error" + }, + { + "method": "", + "name": "errorAcceptGroupAddClientErrors", + "type": "union", + "parserRequired": true, + "unionVariants": [ + { + "name": "IQErrorAlreadyExists", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "already-exists" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "304" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "already-exists" + }, + { + "kind": "attr", + "name": "code", + "value": "304" + } + ] + }, + { + "name": "IQErrorBadRequest", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "bad-request" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "400" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "bad-request" + }, + { + "kind": "attr", + "name": "code", + "value": "400" + } + ] + }, + { + "name": "IQErrorNotAuthorized", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "not-authorized" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "401" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "not-authorized" + }, + { + "kind": "attr", + "name": "code", + "value": "401" + } + ] + }, + { + "name": "IQErrorForbidden", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "forbidden" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "403" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "forbidden" + }, + { + "kind": "attr", + "name": "code", + "value": "403" + } + ] + }, + { + "name": "IQErrorItemNotFound", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "item-not-found" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "404" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "item-not-found" + }, + { + "kind": "attr", + "name": "code", + "value": "404" + } + ] + }, + { + "name": "IQErrorConflict", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "conflict" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "409" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "conflict" + }, + { + "kind": "attr", + "name": "code", + "value": "409" + } + ] + }, + { + "name": "IQErrorGone", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "gone" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "410" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "gone" + }, + { + "kind": "attr", + "name": "code", + "value": "410" + } + ] + }, + { + "name": "IQErrorResourceLimit", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "resource-limit" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "419" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "resource-limit" + }, + { + "kind": "attr", + "name": "code", + "value": "419" + } + ] + }, + { + "name": "IQErrorLocked", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "locked" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "423" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "locked" + }, + { + "kind": "attr", + "name": "code", + "value": "423" + } + ] + }, + { + "name": "IQErrorResourceConstraint", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "resource-constraint" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "500" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "resource-constraint" + }, + { + "kind": "attr", + "name": "code", + "value": "500" + } + ] + }, + { + "name": "IQErrorNotAllowed", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "not-allowed" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "405" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "not-allowed" + }, + { + "kind": "attr", + "name": "code", + "value": "405" + } + ] + }, + { + "name": "IQErrorParentLinkedGroupsParticipantsResourceLimit", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "linked-groups-participants-resource-limit" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "412" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "linked-groups-participants-resource-limit" + }, + { + "kind": "attr", + "name": "code", + "value": "412" + } + ] + }, + { + "name": "IQErrorFallbackClient", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "intMin": 400, + "intMax": 499 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + } + ] + } + ], + "sourcePath": [ + "error" + ] + } + ] + }, + { + "tag": "AcceptGroupAddResponseServerError", + "moduleName": "WASmaxInGroupsAcceptGroupAddResponseServerError", + "kind": "server_error", + "errorArms": [ + { + "name": "IQErrorInternalServerError", + "code": 500, + "text": "internal-server-error" + }, + { + "name": "IQErrorServiceUnavailable", + "code": 503, + "text": "service-unavailable" + }, + { + "name": "IQErrorPartialServerError", + "code": 530, + "text": "partial-server-error" + }, + { + "name": "IQErrorFallbackServer", + "codeMin": 500, + "codeMax": 599 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "error" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "error" + }, + { + "method": "", + "name": "errorServerErrors", + "type": "union", + "parserRequired": true, + "unionVariants": [ + { + "name": "IQErrorInternalServerError", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "internal-server-error" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "500" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "internal-server-error" + }, + { + "kind": "attr", + "name": "code", + "value": "500" + } + ] + }, + { + "name": "IQErrorServiceUnavailable", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "service-unavailable" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "503" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "service-unavailable" + }, + { + "kind": "attr", + "name": "code", + "value": "503" + } + ] + }, + { + "name": "IQErrorPartialServerError", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "partial-server-error" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "530" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "partial-server-error" + }, + { + "kind": "attr", + "name": "code", + "value": "530" + } + ] + }, + { + "name": "IQErrorFallbackServer", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "intMin": 500, + "intMax": 599 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + } + ] + } + ], + "sourcePath": [ + "error" + ] + } + ] + } + ] + } + }, + { + "moduleName": "WASmaxOutGroupsSetSubjectRequest", + "namespace": "w:g2", + "iqType": "set", + "target": "group_jid", + "parserName": "WASmaxGroupsSetSubjectRPC", + "exportedFunction": "makeSetSubjectRequest", + "allExports": [ + "makeSetSubjectRequest" + ], + "request": { + "namespace": "w:g2", + "iqType": "set", + "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], + "children": [ + { + "tag": "subject", + "attrs": [], + "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "subjectElementValue" + } + ] + }, + "repeats": false + } + ] + }, + "response": { + "parserName": "WASmaxGroupsSetSubjectRPC", + "assertions": [], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ], + "variants": [ + { + "tag": "SetSubjectResponseSuccess", + "moduleName": "WASmaxInGroupsSetSubjectResponseSuccess", + "kind": "success", + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "result" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ] + }, + { + "tag": "SetSubjectResponseClientError", + "moduleName": "WASmaxInGroupsSetSubjectResponseClientError", + "kind": "client_error", + "errorArms": [ + { + "name": "IQErrorBadRequest", + "code": 400, + "text": "bad-request" + }, + { + "name": "IQErrorItemNotFound", + "code": 404, + "text": "item-not-found" + }, + { + "name": "IQErrorNotAuthorized", + "code": 401, + "text": "not-authorized" + }, + { + "name": "IQErrorForbidden", + "code": 403, + "text": "forbidden" + }, + { + "name": "IQErrorNotAcceptable", + "code": 406, + "text": "not-acceptable" + }, + { + "name": "IQErrorLocked", + "code": 423, + "text": "locked" + }, + { + "name": "IQErrorRateOverlimit", + "code": 429, + "text": "rate-overlimit" + }, + { + "name": "IQErrorFallbackClient", + "codeMin": 400, + "codeMax": 499 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "error" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "error" + }, + { + "method": "", + "name": "errorSetSubjectClientErrors", + "type": "union", + "parserRequired": true, + "unionVariants": [ + { + "name": "IQErrorBadRequest", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "bad-request" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "400" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "bad-request" + }, + { + "kind": "attr", + "name": "code", + "value": "400" + } + ] + }, + { + "name": "IQErrorItemNotFound", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "item-not-found" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "404" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "item-not-found" + }, + { + "kind": "attr", + "name": "code", + "value": "404" + } + ] + }, + { + "name": "IQErrorNotAuthorized", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "not-authorized" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "401" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "not-authorized" + }, + { + "kind": "attr", + "name": "code", + "value": "401" + } + ] + }, + { + "name": "IQErrorForbidden", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "forbidden" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "403" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "forbidden" + }, + { + "kind": "attr", + "name": "code", + "value": "403" + } + ] + }, + { + "name": "IQErrorNotAcceptable", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "not-acceptable" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "406" + }, + { + "method": "child", + "name": "field", + "type": "string", + "parserRequired": false, + "tag": "field", + "children": [ + { + "method": "attrString", + "name": "name", + "wireName": "name", + "type": "string", + "parserRequired": true + }, + { + "method": "attrString", + "name": "reason", + "wireName": "reason", + "type": "string", + "parserRequired": true + } + ], + "repeats": false + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "not-acceptable" + }, + { + "kind": "attr", + "name": "code", + "value": "406" + } + ] + }, + { + "name": "IQErrorLocked", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "locked" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "423" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "locked" + }, + { + "kind": "attr", + "name": "code", + "value": "423" + } + ] + }, + { + "name": "IQErrorRateOverlimit", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "rate-overlimit" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "429" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "rate-overlimit" + }, + { + "kind": "attr", + "name": "code", + "value": "429" + } + ] + }, + { + "name": "IQErrorFallbackClient", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "intMin": 400, + "intMax": 499 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + } + ] + } + ], + "sourcePath": [ + "error" + ] + } + ] + }, + { + "tag": "SetSubjectResponseServerError", + "moduleName": "WASmaxInGroupsSetSubjectResponseServerError", + "kind": "server_error", + "errorArms": [ + { + "name": "IQErrorInternalServerError", + "code": 500, + "text": "internal-server-error" + }, + { + "name": "IQErrorServiceUnavailable", + "code": 503, + "text": "service-unavailable" + }, + { + "name": "IQErrorPartialServerError", + "code": 530, + "text": "partial-server-error" + }, + { + "name": "IQErrorFallbackServer", + "codeMin": 500, + "codeMax": 599 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "error" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "error" + }, + { + "method": "", + "name": "errorServerErrors", + "type": "union", + "parserRequired": true, + "unionVariants": [ + { + "name": "IQErrorInternalServerError", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "internal-server-error" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "500" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "internal-server-error" + }, + { + "kind": "attr", + "name": "code", + "value": "500" + } + ] + }, + { + "name": "IQErrorServiceUnavailable", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "service-unavailable" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "503" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "service-unavailable" + }, + { + "kind": "attr", + "name": "code", + "value": "503" + } + ] + }, + { + "name": "IQErrorPartialServerError", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "partial-server-error" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "530" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "partial-server-error" + }, + { + "kind": "attr", + "name": "code", + "value": "530" + } + ] + }, + { + "name": "IQErrorFallbackServer", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "intMin": 500, + "intMax": 599 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + } + ] + } + ], + "sourcePath": [ + "error" + ] + } + ] + } + ] + } + } + ], + "unparseable": [] +} diff --git a/tests/conformance/compiled/inputs/2.3000.1047483476.json b/tests/conformance/compiled/inputs/2.3000.1047483476.json new file mode 100644 index 00000000..f7bf7acc --- /dev/null +++ b/tests/conformance/compiled/inputs/2.3000.1047483476.json @@ -0,0 +1,1674 @@ +{ + "schemaVersion": "4.3.0", + "waVersion": "2.3000.1047483476", + "stanzas": [ + { + "moduleName": "WASmaxOutGroupsAcceptGroupAddRequest", + "namespace": "w:g2", + "iqType": "set", + "target": "group_jid", + "parserName": "WASmaxGroupsAcceptGroupAddRPC", + "exportedFunction": "makeAcceptGroupAddRequest", + "allExports": [ + "makeAcceptGroupAddRequest" + ], + "request": { + "namespace": "w:g2", + "iqType": "set", + "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], + "children": [ + { + "tag": "accept", + "attrs": [ + { + "name": "code", + "kind": "string", + "required": true, + "argPath": [ + { + "key": "acceptCode" + } + ] + }, + { + "name": "expiration", + "kind": "integer", + "required": true, + "argPath": [ + { + "key": "acceptExpiration" + } + ] + }, + { + "name": "admin", + "kind": "user_jid", + "required": true, + "argPath": [ + { + "key": "acceptAdmin" + } + ] + } + ], + "children": [], + "repeats": false + } + ] + }, + "response": { + "parserName": "WASmaxGroupsAcceptGroupAddRPC", + "assertions": [], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ], + "variants": [ + { + "tag": "AcceptGroupAddResponseGroupJoinRequestSuccess", + "moduleName": "WASmaxInGroupsAcceptGroupAddResponseGroupJoinRequestSuccess", + "kind": "success", + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "result" + }, + { + "kind": "child", + "name": "membership_approval_request" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ] + }, + { + "tag": "AcceptGroupAddResponseSuccess", + "moduleName": "WASmaxInGroupsAcceptGroupAddResponseSuccess", + "kind": "success", + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "result" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ] + }, + { + "tag": "AcceptGroupAddResponseClientError", + "moduleName": "WASmaxInGroupsAcceptGroupAddResponseClientError", + "kind": "error", + "errorArms": [ + { + "name": "IQErrorAlreadyExists", + "code": 304, + "text": "already-exists" + }, + { + "name": "IQErrorBadRequest", + "code": 400, + "text": "bad-request" + }, + { + "name": "IQErrorNotAuthorized", + "code": 401, + "text": "not-authorized" + }, + { + "name": "IQErrorForbidden", + "code": 403, + "text": "forbidden" + }, + { + "name": "IQErrorItemNotFound", + "code": 404, + "text": "item-not-found" + }, + { + "name": "IQErrorConflict", + "code": 409, + "text": "conflict" + }, + { + "name": "IQErrorGone", + "code": 410, + "text": "gone" + }, + { + "name": "IQErrorResourceLimit", + "code": 419, + "text": "resource-limit" + }, + { + "name": "IQErrorLocked", + "code": 423, + "text": "locked" + }, + { + "name": "IQErrorResourceConstraint", + "code": 500, + "text": "resource-constraint" + }, + { + "name": "IQErrorNotAllowed", + "code": 405, + "text": "not-allowed" + }, + { + "name": "IQErrorParentLinkedGroupsParticipantsResourceLimit", + "code": 412, + "text": "linked-groups-participants-resource-limit" + }, + { + "name": "IQErrorFallbackClient", + "codeMin": 400, + "codeMax": 499 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "error" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "error" + }, + { + "method": "", + "name": "errorAcceptGroupAddClientErrors", + "type": "union", + "parserRequired": true, + "unionVariants": [ + { + "name": "IQErrorAlreadyExists", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "already-exists" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "304" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "already-exists" + }, + { + "kind": "attr", + "name": "code", + "value": "304" + } + ] + }, + { + "name": "IQErrorBadRequest", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "bad-request" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "400" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "bad-request" + }, + { + "kind": "attr", + "name": "code", + "value": "400" + } + ] + }, + { + "name": "IQErrorNotAuthorized", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "not-authorized" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "401" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "not-authorized" + }, + { + "kind": "attr", + "name": "code", + "value": "401" + } + ] + }, + { + "name": "IQErrorForbidden", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "forbidden" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "403" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "forbidden" + }, + { + "kind": "attr", + "name": "code", + "value": "403" + } + ] + }, + { + "name": "IQErrorItemNotFound", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "item-not-found" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "404" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "item-not-found" + }, + { + "kind": "attr", + "name": "code", + "value": "404" + } + ] + }, + { + "name": "IQErrorConflict", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "conflict" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "409" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "conflict" + }, + { + "kind": "attr", + "name": "code", + "value": "409" + } + ] + }, + { + "name": "IQErrorGone", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "gone" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "410" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "gone" + }, + { + "kind": "attr", + "name": "code", + "value": "410" + } + ] + }, + { + "name": "IQErrorResourceLimit", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "resource-limit" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "419" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "resource-limit" + }, + { + "kind": "attr", + "name": "code", + "value": "419" + } + ] + }, + { + "name": "IQErrorLocked", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "locked" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "423" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "locked" + }, + { + "kind": "attr", + "name": "code", + "value": "423" + } + ] + }, + { + "name": "IQErrorResourceConstraint", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "resource-constraint" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "500" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "resource-constraint" + }, + { + "kind": "attr", + "name": "code", + "value": "500" + } + ] + }, + { + "name": "IQErrorNotAllowed", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "not-allowed" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "405" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "not-allowed" + }, + { + "kind": "attr", + "name": "code", + "value": "405" + } + ] + }, + { + "name": "IQErrorParentLinkedGroupsParticipantsResourceLimit", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "linked-groups-participants-resource-limit" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "412" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "linked-groups-participants-resource-limit" + }, + { + "kind": "attr", + "name": "code", + "value": "412" + } + ] + }, + { + "name": "IQErrorFallbackClient", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "intMin": 400, + "intMax": 499 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + } + ] + } + ], + "sourcePath": [ + "error" + ] + } + ] + }, + { + "tag": "AcceptGroupAddResponseServerError", + "moduleName": "WASmaxInGroupsAcceptGroupAddResponseServerError", + "kind": "server_error", + "errorArms": [ + { + "name": "IQErrorInternalServerError", + "code": 500, + "text": "internal-server-error" + }, + { + "name": "IQErrorServiceUnavailable", + "code": 503, + "text": "service-unavailable" + }, + { + "name": "IQErrorPartialServerError", + "code": 530, + "text": "partial-server-error" + }, + { + "name": "IQErrorFallbackServer", + "codeMin": 500, + "codeMax": 599 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "error" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "error" + }, + { + "method": "", + "name": "errorServerErrors", + "type": "union", + "parserRequired": true, + "unionVariants": [ + { + "name": "IQErrorInternalServerError", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "internal-server-error" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "500" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "internal-server-error" + }, + { + "kind": "attr", + "name": "code", + "value": "500" + } + ] + }, + { + "name": "IQErrorServiceUnavailable", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "service-unavailable" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "503" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "service-unavailable" + }, + { + "kind": "attr", + "name": "code", + "value": "503" + } + ] + }, + { + "name": "IQErrorPartialServerError", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "partial-server-error" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "530" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "partial-server-error" + }, + { + "kind": "attr", + "name": "code", + "value": "530" + } + ] + }, + { + "name": "IQErrorFallbackServer", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "intMin": 500, + "intMax": 599 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + } + ] + } + ], + "sourcePath": [ + "error" + ] + } + ] + } + ] + } + }, + { + "moduleName": "WASmaxOutGroupsSetSubjectRequest", + "namespace": "w:g2", + "iqType": "set", + "target": "group_jid", + "parserName": "WASmaxGroupsSetSubjectRPC", + "exportedFunction": "makeSetSubjectRequest", + "allExports": [ + "makeSetSubjectRequest" + ], + "request": { + "namespace": "w:g2", + "iqType": "set", + "target": "group_jid", + "targetArgPath": [ + { + "key": "iqTo" + } + ], + "children": [ + { + "tag": "subject", + "attrs": [], + "children": [], + "content": { + "kind": "dynamic", + "argPath": [ + { + "key": "subjectElementValue" + } + ] + }, + "repeats": false + } + ] + }, + "response": { + "parserName": "WASmaxGroupsSetSubjectRPC", + "assertions": [], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ], + "variants": [ + { + "tag": "SetSubjectResponseSuccess", + "moduleName": "WASmaxInGroupsSetSubjectResponseSuccess", + "kind": "success", + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "result" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "result" + } + ] + }, + { + "tag": "SetSubjectResponseClientError", + "moduleName": "WASmaxInGroupsSetSubjectResponseClientError", + "kind": "client_error", + "errorArms": [ + { + "name": "IQErrorBadRequest", + "code": 400, + "text": "bad-request" + }, + { + "name": "IQErrorItemNotFound", + "code": 404, + "text": "item-not-found" + }, + { + "name": "IQErrorNotAuthorized", + "code": 401, + "text": "not-authorized" + }, + { + "name": "IQErrorForbidden", + "code": 403, + "text": "forbidden" + }, + { + "name": "IQErrorNotAcceptable", + "code": 406, + "text": "not-acceptable" + }, + { + "name": "IQErrorLocked", + "code": 423, + "text": "locked" + }, + { + "name": "IQErrorRateOverlimit", + "code": 429, + "text": "rate-overlimit" + }, + { + "name": "IQErrorFallbackClient", + "codeMin": 400, + "codeMax": 499 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "error" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "error" + }, + { + "method": "", + "name": "errorSetSubjectClientErrors", + "type": "union", + "parserRequired": true, + "unionVariants": [ + { + "name": "IQErrorBadRequest", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "bad-request" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "400" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "bad-request" + }, + { + "kind": "attr", + "name": "code", + "value": "400" + } + ] + }, + { + "name": "IQErrorItemNotFound", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "item-not-found" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "404" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "item-not-found" + }, + { + "kind": "attr", + "name": "code", + "value": "404" + } + ] + }, + { + "name": "IQErrorNotAuthorized", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "not-authorized" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "401" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "not-authorized" + }, + { + "kind": "attr", + "name": "code", + "value": "401" + } + ] + }, + { + "name": "IQErrorForbidden", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "forbidden" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "403" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "forbidden" + }, + { + "kind": "attr", + "name": "code", + "value": "403" + } + ] + }, + { + "name": "IQErrorNotAcceptable", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "not-acceptable" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "406" + }, + { + "method": "child", + "name": "field", + "type": "string", + "parserRequired": false, + "tag": "field", + "children": [ + { + "method": "attrString", + "name": "name", + "wireName": "name", + "type": "string", + "parserRequired": true + }, + { + "method": "attrString", + "name": "reason", + "wireName": "reason", + "type": "string", + "parserRequired": true + } + ], + "repeats": false + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "not-acceptable" + }, + { + "kind": "attr", + "name": "code", + "value": "406" + } + ] + }, + { + "name": "IQErrorLocked", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "locked" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "423" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "locked" + }, + { + "kind": "attr", + "name": "code", + "value": "423" + } + ] + }, + { + "name": "IQErrorRateOverlimit", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "rate-overlimit" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "429" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "rate-overlimit" + }, + { + "kind": "attr", + "name": "code", + "value": "429" + } + ] + }, + { + "name": "IQErrorFallbackClient", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "intMin": 400, + "intMax": 499 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + } + ] + } + ], + "sourcePath": [ + "error" + ] + } + ] + }, + { + "tag": "SetSubjectResponseServerError", + "moduleName": "WASmaxInGroupsSetSubjectResponseServerError", + "kind": "server_error", + "errorArms": [ + { + "name": "IQErrorInternalServerError", + "code": 500, + "text": "internal-server-error" + }, + { + "name": "IQErrorServiceUnavailable", + "code": 503, + "text": "service-unavailable" + }, + { + "name": "IQErrorPartialServerError", + "code": 530, + "text": "partial-server-error" + }, + { + "name": "IQErrorFallbackServer", + "codeMin": 500, + "codeMax": 599 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "iq" + }, + { + "kind": "reference", + "name": "id", + "referencePath": [ + "id" + ] + }, + { + "kind": "reference", + "name": "from", + "referencePath": [ + "to" + ] + }, + { + "kind": "attr", + "name": "type", + "value": "error" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "parserRequired": true, + "literalValue": "error" + }, + { + "method": "", + "name": "errorServerErrors", + "type": "union", + "parserRequired": true, + "unionVariants": [ + { + "name": "IQErrorInternalServerError", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "internal-server-error" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "500" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "internal-server-error" + }, + { + "kind": "attr", + "name": "code", + "value": "500" + } + ] + }, + { + "name": "IQErrorServiceUnavailable", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "service-unavailable" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "503" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "service-unavailable" + }, + { + "kind": "attr", + "name": "code", + "value": "503" + } + ] + }, + { + "name": "IQErrorPartialServerError", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true, + "literalValue": "partial-server-error" + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "literalValue": "530" + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + }, + { + "kind": "attr", + "name": "text", + "value": "partial-server-error" + }, + { + "kind": "attr", + "name": "code", + "value": "530" + } + ] + }, + { + "name": "IQErrorFallbackServer", + "fields": [ + { + "method": "attrString", + "name": "text", + "wireName": "text", + "type": "string", + "parserRequired": true + }, + { + "method": "attrInt", + "name": "code", + "wireName": "code", + "type": "integer", + "parserRequired": true, + "intMin": 500, + "intMax": 599 + } + ], + "assertions": [ + { + "kind": "tag", + "name": "error" + } + ] + } + ], + "sourcePath": [ + "error" + ] + } + ] + } + ] + } + } + ], + "unparseable": [] +} diff --git a/tests/conformance/compiled/inputs/provenance.json b/tests/conformance/compiled/inputs/provenance.json new file mode 100644 index 00000000..551caa72 --- /dev/null +++ b/tests/conformance/compiled/inputs/provenance.json @@ -0,0 +1,16 @@ +[ + { + "commit": "1a441f0329c941fcdb238490a6c604550d8a9939", + "waVersion": "2.3000.1045368834", + "sourcePath": "generated/iq/index.json", + "sourceSha256": "209d5d5468892bda7671e7fae77b581a9890cbd02f4d47c0de83dc7f0af7c81c", + "selectedInputSha256": "0c1a77e24d079cf8f1d00c82f87530c3a93bac297573320a824289939502fd5f" + }, + { + "commit": "1f5167c4cebf263edaf001e06a2497f213c55793", + "waVersion": "2.3000.1047483476", + "sourcePath": "generated/iq/index.json", + "sourceSha256": "f7251fd9aaa07062495e431a80d40dcdb44ffd8fb68b1776d65c41b92e29aafd", + "selectedInputSha256": "3190c84c2acdb39f43e52068be51b86c3aa597566738b573b3498937e6a0ca14" + } +] diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddClientErrors.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddClientErrors.js new file mode 100644 index 00000000..5276e937 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddClientErrors.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddClientErrors",["WAResultOrError","WASmaxInGroupsIQErrorAlreadyExistsMixin","WASmaxInGroupsIQErrorBadRequestMixin","WASmaxInGroupsIQErrorConflictMixin","WASmaxInGroupsIQErrorFallbackClientMixin","WASmaxInGroupsIQErrorForbiddenMixin","WASmaxInGroupsIQErrorGoneMixin","WASmaxInGroupsIQErrorItemNotFoundMixin","WASmaxInGroupsIQErrorLockedMixin","WASmaxInGroupsIQErrorNotAllowedMixin","WASmaxInGroupsIQErrorNotAuthorizedMixin","WASmaxInGroupsIQErrorParentLinkedGroupsParticipantsResourceLimitMixin","WASmaxInGroupsIQErrorResourceConstraintMixin","WASmaxInGroupsIQErrorResourceLimitMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxInGroupsIQErrorAlreadyExistsMixin").parseIQErrorAlreadyExistsMixin(e);if(t.success)return o("WAResultOrError").makeResult({name:"IQErrorAlreadyExists",value:t.value});var n=o("WASmaxInGroupsIQErrorBadRequestMixin").parseIQErrorBadRequestMixin(e);if(n.success)return o("WAResultOrError").makeResult({name:"IQErrorBadRequest",value:n.value});var r=o("WASmaxInGroupsIQErrorNotAuthorizedMixin").parseIQErrorNotAuthorizedMixin(e);if(r.success)return o("WAResultOrError").makeResult({name:"IQErrorNotAuthorized",value:r.value});var a=o("WASmaxInGroupsIQErrorForbiddenMixin").parseIQErrorForbiddenMixin(e);if(a.success)return o("WAResultOrError").makeResult({name:"IQErrorForbidden",value:a.value});var i=o("WASmaxInGroupsIQErrorItemNotFoundMixin").parseIQErrorItemNotFoundMixin(e);if(i.success)return o("WAResultOrError").makeResult({name:"IQErrorItemNotFound",value:i.value});var l=o("WASmaxInGroupsIQErrorConflictMixin").parseIQErrorConflictMixin(e);if(l.success)return o("WAResultOrError").makeResult({name:"IQErrorConflict",value:l.value});var s=o("WASmaxInGroupsIQErrorGoneMixin").parseIQErrorGoneMixin(e);if(s.success)return o("WAResultOrError").makeResult({name:"IQErrorGone",value:s.value});var u=o("WASmaxInGroupsIQErrorResourceLimitMixin").parseIQErrorResourceLimitMixin(e);if(u.success)return o("WAResultOrError").makeResult({name:"IQErrorResourceLimit",value:u.value});var c=o("WASmaxInGroupsIQErrorLockedMixin").parseIQErrorLockedMixin(e);if(c.success)return o("WAResultOrError").makeResult({name:"IQErrorLocked",value:c.value});var d=o("WASmaxInGroupsIQErrorResourceConstraintMixin").parseIQErrorResourceConstraintMixin(e);if(d.success)return o("WAResultOrError").makeResult({name:"IQErrorResourceConstraint",value:d.value});var m=o("WASmaxInGroupsIQErrorNotAllowedMixin").parseIQErrorNotAllowedMixin(e);if(m.success)return o("WAResultOrError").makeResult({name:"IQErrorNotAllowed",value:m.value});var p=o("WASmaxInGroupsIQErrorParentLinkedGroupsParticipantsResourceLimitMixin").parseIQErrorParentLinkedGroupsParticipantsResourceLimitMixin(e);if(p.success)return o("WAResultOrError").makeResult({name:"IQErrorParentLinkedGroupsParticipantsResourceLimit",value:p.value});var _=o("WASmaxInGroupsIQErrorFallbackClientMixin").parseIQErrorFallbackClientMixin(e);return _.success?o("WAResultOrError").makeResult({name:"IQErrorFallbackClient",value:_.value}):o("WASmaxParseUtils").errorMixinDisjunction(e,["IQErrorAlreadyExists","IQErrorBadRequest","IQErrorNotAuthorized","IQErrorForbidden","IQErrorItemNotFound","IQErrorConflict","IQErrorGone","IQErrorResourceLimit","IQErrorLocked","IQErrorResourceConstraint","IQErrorNotAllowed","IQErrorParentLinkedGroupsParticipantsResourceLimit","IQErrorFallbackClient"],[t,n,r,a,i,l,s,u,c,d,m,p,_])}l.parseAcceptGroupAddClientErrors=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseClientError.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseClientError.js new file mode 100644 index 00000000..db464e78 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseClientError.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddResponseClientError",["WAResultOrError","WASmaxInGroupsAcceptGroupAddClientErrors","WASmaxInGroupsIQErrorResponseMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseUtils").flattenedChildWithTag(e,"error");if(!r.success)return r;var a=o("WASmaxInGroupsIQErrorResponseMixin").parseIQErrorResponseMixin(e,t);if(!a.success)return a;var i=o("WASmaxInGroupsAcceptGroupAddClientErrors").parseAcceptGroupAddClientErrors(r.value);return i.success?o("WAResultOrError").makeResult(babelHelpers.extends({},a.value,{errorAcceptGroupAddClientErrors:i.value})):i}l.parseAcceptGroupAddResponseClientError=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseServerError.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseServerError.js new file mode 100644 index 00000000..596bd2b0 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsAcceptGroupAddResponseServerError.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddResponseServerError",["WASmaxInGroupsBaseServerErrorMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxInGroupsBaseServerErrorMixin").parseBaseServerErrorMixin(e,t);return r.success,r}l.parseAcceptGroupAddResponseServerError=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorAlreadyExistsMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorAlreadyExistsMixin.js new file mode 100644 index 00000000..c8ffd4f9 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorAlreadyExistsMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorAlreadyExistsMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","already-exists");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",304);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorAlreadyExistsMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorBadRequestMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorBadRequestMixin.js new file mode 100644 index 00000000..b656b821 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorBadRequestMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorBadRequestMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","bad-request");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",400);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorBadRequestMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorFallbackClientMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorFallbackClientMixin.js new file mode 100644 index 00000000..baa00afb --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorFallbackClientMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorFallbackClientMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").attrString(e,"text");if(!n.success)return n;var r=o("WASmaxParseUtils").attrIntRange(e,"code",400,499);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorFallbackClientMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorFallbackServerMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorFallbackServerMixin.js new file mode 100644 index 00000000..0c4b94d3 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorFallbackServerMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorFallbackServerMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").attrString(e,"text");if(!n.success)return n;var r=o("WASmaxParseUtils").attrIntRange(e,"code",500,599);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorFallbackServerMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorInternalServerErrorMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorInternalServerErrorMixin.js new file mode 100644 index 00000000..6bd4c807 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorInternalServerErrorMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorInternalServerErrorMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","internal-server-error");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",500);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorInternalServerErrorMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorNotAcceptableMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorNotAcceptableMixin.js new file mode 100644 index 00000000..f1c2016b --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorNotAcceptableMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorNotAcceptableMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"field");if(!t.success)return t;var n=o("WASmaxParseUtils").attrString(e,"name");if(!n.success)return n;var r=o("WASmaxParseUtils").attrString(e,"reason");return r.success?o("WAResultOrError").makeResult({name:n.value,reason:r.value}):r}function s(t){var n=o("WASmaxParseUtils").assertTag(t,"error");if(!n.success)return n;var r=o("WASmaxParseUtils").optionalChildWithTag(t,"field",e);if(!r.success)return r;var a=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,t,"text","not-acceptable");if(!a.success)return a;var i=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,t,"code",406);return i.success?o("WAResultOrError").makeResult({text:a.value,code:i.value,field:r.value}):i}l.parseIQErrorNotAcceptableField=e,l.parseIQErrorNotAcceptableMixin=s}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorPartialServerErrorMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorPartialServerErrorMixin.js new file mode 100644 index 00000000..26e39dc5 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorPartialServerErrorMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorPartialServerErrorMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","partial-server-error");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",530);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorPartialServerErrorMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorResourceConstraintMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorResourceConstraintMixin.js new file mode 100644 index 00000000..b1bc3e4c --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorResourceConstraintMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorResourceConstraintMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","resource-constraint");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",500);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorResourceConstraintMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorResponseMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorResponseMixin.js new file mode 100644 index 00000000..c5d71a87 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorResponseMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorResponseMixin",["WAResultOrError","WASmaxParseReference","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseReference").attrStringFromReference(t,["id"]);if(!r.success)return r;var a=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"id",r.value);if(!a.success)return a;var i=o("WASmaxParseReference").attrStringFromReference(t,["to"]);if(!i.success)return i;var l=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"from",i.value);if(!l.success)return l;var s=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"type","error");return s.success?o("WAResultOrError").makeResult({type:s.value}):s}l.parseIQErrorResponseMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorServiceUnavailableMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorServiceUnavailableMixin.js new file mode 100644 index 00000000..9a98821b --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsIQErrorServiceUnavailableMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorServiceUnavailableMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","service-unavailable");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",503);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorServiceUnavailableMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsServerErrors.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsServerErrors.js new file mode 100644 index 00000000..ad19360e --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsServerErrors.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsServerErrors",["WAResultOrError","WASmaxInGroupsIQErrorFallbackServerMixin","WASmaxInGroupsIQErrorInternalServerErrorMixin","WASmaxInGroupsIQErrorPartialServerErrorMixin","WASmaxInGroupsIQErrorServiceUnavailableMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxInGroupsIQErrorInternalServerErrorMixin").parseIQErrorInternalServerErrorMixin(e);if(t.success)return o("WAResultOrError").makeResult({name:"IQErrorInternalServerError",value:t.value});var n=o("WASmaxInGroupsIQErrorServiceUnavailableMixin").parseIQErrorServiceUnavailableMixin(e);if(n.success)return o("WAResultOrError").makeResult({name:"IQErrorServiceUnavailable",value:n.value});var r=o("WASmaxInGroupsIQErrorPartialServerErrorMixin").parseIQErrorPartialServerErrorMixin(e);if(r.success)return o("WAResultOrError").makeResult({name:"IQErrorPartialServerError",value:r.value});var a=o("WASmaxInGroupsIQErrorFallbackServerMixin").parseIQErrorFallbackServerMixin(e);return a.success?o("WAResultOrError").makeResult({name:"IQErrorFallbackServer",value:a.value}):o("WASmaxParseUtils").errorMixinDisjunction(e,["IQErrorInternalServerError","IQErrorServiceUnavailable","IQErrorPartialServerError","IQErrorFallbackServer"],[t,n,r,a])}l.parseServerErrors=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectClientErrors.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectClientErrors.js new file mode 100644 index 00000000..4fb442a4 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectClientErrors.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsSetSubjectClientErrors",["WAResultOrError","WASmaxInGroupsIQErrorBadRequestMixin","WASmaxInGroupsIQErrorFallbackClientMixin","WASmaxInGroupsIQErrorForbiddenMixin","WASmaxInGroupsIQErrorItemNotFoundMixin","WASmaxInGroupsIQErrorLockedMixin","WASmaxInGroupsIQErrorNotAcceptableMixin","WASmaxInGroupsIQErrorNotAuthorizedMixin","WASmaxInGroupsIQErrorRateOverlimitMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxInGroupsIQErrorBadRequestMixin").parseIQErrorBadRequestMixin(e);if(t.success)return o("WAResultOrError").makeResult({name:"IQErrorBadRequest",value:t.value});var n=o("WASmaxInGroupsIQErrorItemNotFoundMixin").parseIQErrorItemNotFoundMixin(e);if(n.success)return o("WAResultOrError").makeResult({name:"IQErrorItemNotFound",value:n.value});var r=o("WASmaxInGroupsIQErrorNotAuthorizedMixin").parseIQErrorNotAuthorizedMixin(e);if(r.success)return o("WAResultOrError").makeResult({name:"IQErrorNotAuthorized",value:r.value});var a=o("WASmaxInGroupsIQErrorForbiddenMixin").parseIQErrorForbiddenMixin(e);if(a.success)return o("WAResultOrError").makeResult({name:"IQErrorForbidden",value:a.value});var i=o("WASmaxInGroupsIQErrorNotAcceptableMixin").parseIQErrorNotAcceptableMixin(e);if(i.success)return o("WAResultOrError").makeResult({name:"IQErrorNotAcceptable",value:i.value});var l=o("WASmaxInGroupsIQErrorLockedMixin").parseIQErrorLockedMixin(e);if(l.success)return o("WAResultOrError").makeResult({name:"IQErrorLocked",value:l.value});var s=o("WASmaxInGroupsIQErrorRateOverlimitMixin").parseIQErrorRateOverlimitMixin(e);if(s.success)return o("WAResultOrError").makeResult({name:"IQErrorRateOverlimit",value:s.value});var u=o("WASmaxInGroupsIQErrorFallbackClientMixin").parseIQErrorFallbackClientMixin(e);return u.success?o("WAResultOrError").makeResult({name:"IQErrorFallbackClient",value:u.value}):o("WASmaxParseUtils").errorMixinDisjunction(e,["IQErrorBadRequest","IQErrorItemNotFound","IQErrorNotAuthorized","IQErrorForbidden","IQErrorNotAcceptable","IQErrorLocked","IQErrorRateOverlimit","IQErrorFallbackClient"],[t,n,r,a,i,l,s,u])}l.parseSetSubjectClientErrors=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseClientError.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseClientError.js new file mode 100644 index 00000000..3a007da1 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseClientError.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsSetSubjectResponseClientError",["WAResultOrError","WASmaxInGroupsIQErrorResponseMixin","WASmaxInGroupsSetSubjectClientErrors","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseUtils").flattenedChildWithTag(e,"error");if(!r.success)return r;var a=o("WASmaxInGroupsIQErrorResponseMixin").parseIQErrorResponseMixin(e,t);if(!a.success)return a;var i=o("WASmaxInGroupsSetSubjectClientErrors").parseSetSubjectClientErrors(r.value);return i.success?o("WAResultOrError").makeResult(babelHelpers.extends({},a.value,{errorSetSubjectClientErrors:i.value})):i}l.parseSetSubjectResponseClientError=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseServerError.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseServerError.js new file mode 100644 index 00000000..89f5986f --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsSetSubjectResponseServerError.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsSetSubjectResponseServerError",["WASmaxInGroupsBaseServerErrorMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxInGroupsBaseServerErrorMixin").parseBaseServerErrorMixin(e,t);return r.success,r}l.parseSetSubjectResponseServerError=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxParseReference.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxParseReference.js new file mode 100644 index 00000000..4db455e8 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxParseReference.js @@ -0,0 +1 @@ +__d("WASmaxParseReference",["WAHasProperty","WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){"use strict";function e(e,t,n,r,o){var a=m(t,n);if(!a.success)return _(a);var i=e(a.value,n[n.length-1],r,o);return i.success?i:_(i)}function s(t,n,r,a,i){return p(n,r)?e(t,n,r,a,i):o("WASmaxParseUtils").voidSuccess}function u(t,n){return e(o("WASmaxParseUtils").attrString,t,n)}function c(e,t){return p(e,t)?u(e,t):o("WASmaxParseUtils").voidSuccess}function d(e,t){var n=m(e,t);if(!n.success)return _(n);var r=o("WASmaxParseUtils").contentString(n.value);return r.success?r:_(r)}function m(e,t){for(var n=t.length,r=e,a=0;a"):e}function u(e,t){return B(e,t)?W(e,e.attrs[t]):O(e,'to have attribute "'+t+'"')}function c(e,t,n,r){var a=u(e,t);if(!a.success)return a;var i=n(a.value);return i!=null?o("WAResultOrError").makeResult(i):O(e,'to have "'+t+'"={'+r+'}, but instead has "'+a.value+'"')}function d(e,t,n){var r=P(e);if(!r.success)return r;var a=t(r.value);return a!=null?o("WAResultOrError").makeResult(a):O(e,"to have "+n+' content, but instead has "'+r.value+'"')}function m(t,n,r){var o=u(t,n);if(o.success){if(o.value!==r)return O(t,'to have "'+n+'"="'+r+'", but instead has "'+o.value+'"')}else return o;return e}function p(e,t){return c(e,t,o("WAStanzaUtils").toStanzaId,"stanzaID")}function _(e,t){return c(e,t,o("WAStanzaUtils").validateCallId,"callID")}function f(e,t){return c(e,t,q,"integer")}function g(e,t,n,r){var a=f(e,t);if(!a.success)return a;var i=a.value;return n!==void 0&&ir?O(e,'to have "'+t+'"={at most '+r+"} but has value "+i):o("WAResultOrError").makeResult(i)}function h(e){var t=e.content;return t instanceof Uint8Array?O(e,"to have children"):o("WAResultOrError").makeResult(t)}function y(t,n){var r=h(t);if(!r.success)return r;var a=r.value;if(a==null)return e;for(var i=null,l=0;l, but found more than 1");i=s}}return o("WAResultOrError").makeResult(i)}function C(t,n,r){var o=y(t,n);return o.success?o.value==null?e:r(o.value):o}function b(e,t,n){var r=C(e,t,n);if(!r.success)return r;var a=r.value;return a==null?O(e,"to have 1 child <"+t+">, but found 0"):o("WAResultOrError").makeResult(a)}function v(e,t){var n=y(e,t);return n.success?n.value==null?O(e,"to have 1 child <"+t+">, but found 0"):o("WAResultOrError").makeResult(n.value):n}function S(e,t,n,r,a){var i=U(e,t,n,r);if(!i.success)return i;for(var l=[],s=0;sn?O(e,"to have binary content at most "+n+" bytes but has "+a+" bytes"):o("WAResultOrError").makeResult(r)}function A(e){return d(e,q,"integer")}function F(e){return e}function O(e,t){return o("WAResultOrError").makeError("expected <"+e.tag+">: "+t)}function B(e,t){return r("WAHasProperty")(e.attrs,t)}function W(e,t){return t instanceof o("WAWapJid").WapJid?o("WAResultOrError").makeResult(t.toString()):typeof t=="string"?o("WAResultOrError").makeResult(t):O(e,"decodeAsString: attribute is "+typeof t+" not a string: "+String(t))}function q(e){var t=parseInt(e,10);return Number.isNaN(t)?null:t}function U(e,t,n,r){var a=h(e);if(!a.success)return a;var i=a.value;if(i==null)return n!==0?O(e,"to have at least "+n+" <"+t+"> children, but found 0"):o("WAResultOrError").makeResult([]);for(var l=[],s=0;s children, but found "+c):c>r?O(e,"to have at most "+r+" <"+t+"> children, but found "+c):o("WAResultOrError").makeResult(l)}function V(e,t,n){var r=t.map(function(e,t){return e+": "+n[t].error});return O(e,["to match any of following mixins: "+t.join(", ")+", but all mixins failed."].concat(r).join(" "))}l.voidSuccess=e,l.assertTag=s,l.attrString=u,l.attrValidate=c,l.assertAttr=m,l.attrStanzaId=p,l.attrCallId=_,l.attrInt=f,l.attrIntRange=g,l.maybeChildren=h,l.optionalChild=y,l.optionalChildWithTag=C,l.childWithTag=b,l.flattenedChildWithTag=v,l.mapChildrenWithTag=S,l.countChildrenWithTag=R,l.mapHomogeneousChildrenWithTag=L,l.countHomogeneousChildrenWithTag=E,l.optional=k,l.optionalLiteral=I,l.literal=T,l.literalContent=D,l.attrStringEnum=x,l.contentStringEnum=$,l.contentString=P,l.contentBytes=N,l.contentLiteralBytes=M,l.contentBytesRange=w,l.contentInt=A,l.identity=F,l.errorMessage=O,l.errorMixinDisjunction=V}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/provenance.json b/tests/conformance/compiled/sources/2.3000.1045368834/provenance.json new file mode 100644 index 00000000..f0e3e372 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/provenance.json @@ -0,0 +1,141 @@ +{ + "waVersion": "2.3000.1045368834", + "setHash": "99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00", + "bundleCount": 516, + "bundleBytes": 77347149, + "modules": [ + { + "module": "WASmaxInGroupsAcceptGroupAddClientErrors", + "bundleSha256": "91d65febb587cea7283313c48528407fce6e949cdcb7a29cd2e0d9df1d21b1a8", + "start": 19, + "end": 3490, + "sourceSha256": "0bf34d35f47688d83e1703ff785f033e74974a5bb6b27fcc13dfd9a202b10604" + }, + { + "module": "WASmaxInGroupsAcceptGroupAddResponseClientError", + "bundleSha256": "91d65febb587cea7283313c48528407fce6e949cdcb7a29cd2e0d9df1d21b1a8", + "start": 3492, + "end": 4227, + "sourceSha256": "0a34c41c5e0bdc020ddac5ea1a75ea3f957803f66651a477f96e2234447c80a4" + }, + { + "module": "WASmaxInGroupsAcceptGroupAddResponseServerError", + "bundleSha256": "91d65febb587cea7283313c48528407fce6e949cdcb7a29cd2e0d9df1d21b1a8", + "start": 4730, + "end": 5096, + "sourceSha256": "0786f985a20352044f3f00cec811911fce0f002f4dedafdf3b37e1cd087cbab6" + }, + { + "module": "WASmaxInGroupsIQErrorAlreadyExistsMixin", + "bundleSha256": "cf6197f616a43cf9004101d911d5643e8fa63972b2d27a54e8bba8db062abb8f", + "start": 68535, + "end": 69050, + "sourceSha256": "0c207f6532b9d3ab012f556fd7725de77bfdd36289665e3b8e013f31f2c42f6d" + }, + { + "module": "WASmaxInGroupsIQErrorBadRequestMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 29804, + "end": 30310, + "sourceSha256": "fc526eb6a0bb5f2d142d2215e61946163ffc0939eb67ea42a64f0d5aecf4e710" + }, + { + "module": "WASmaxInGroupsIQErrorFallbackClientMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 85970, + "end": 86419, + "sourceSha256": "2cf97c340ea2ebfac9c4c6b94933ebc71a7874ee249ac33ae68d920a60e5813f" + }, + { + "module": "WASmaxInGroupsIQErrorFallbackServerMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 33089, + "end": 33538, + "sourceSha256": "c6ddb72b768a67e8fdaee58b037b2de66c896c4ecb95d67d7d8bed6a64af0bf7" + }, + { + "module": "WASmaxInGroupsIQErrorInternalServerErrorMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 33540, + "end": 34074, + "sourceSha256": "3f728f0545d258df831a6c1a01f9f39c3040c3a4a04b931ee509ccbbc080b4a2" + }, + { + "module": "WASmaxInGroupsIQErrorNotAcceptableMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 88946, + "end": 89886, + "sourceSha256": "88e536fc9040f47de39a3f98ed5f299e9bffb37baa0590ec43ee619061802d56" + }, + { + "module": "WASmaxInGroupsIQErrorPartialServerErrorMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 34076, + "end": 34607, + "sourceSha256": "175917f45fe458ff9b1c229cb025661a7c0fac5664dc0ce983e5f8df44b5da67" + }, + { + "module": "WASmaxInGroupsIQErrorResourceConstraintMixin", + "bundleSha256": "cf6197f616a43cf9004101d911d5643e8fa63972b2d27a54e8bba8db062abb8f", + "start": 69656, + "end": 70186, + "sourceSha256": "00381d7ab0263c85d58b2c01f4f3bf446cc127c51322bbbd04fa3baf07fc3317" + }, + { + "module": "WASmaxInGroupsIQErrorResponseMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 31534, + "end": 32332, + "sourceSha256": "867cb414eafa8d1ddfec085c39696220e7fa07894dd27b8404a9c2ff7f3408e6" + }, + { + "module": "WASmaxInGroupsIQErrorServiceUnavailableMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 34609, + "end": 35139, + "sourceSha256": "6d1e9bb5f7a3f4afb0a1bc90f2f6e1a94f96c305d9813540e9a2cf917196b713" + }, + { + "module": "WASmaxInGroupsServerErrors", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 35141, + "end": 36408, + "sourceSha256": "4ff5d583d148680474bb75b36a6c8fc431d303ddd952e49b94d02d93a3a480d8" + }, + { + "module": "WASmaxInGroupsSetSubjectClientErrors", + "bundleSha256": "ed2f08bc12a1128a06ddafe25fc62e5d102593245a8b5e818fd8f2eedab3ffb9", + "start": 24289, + "end": 26417, + "sourceSha256": "0ee8d5ffc0942026ba3ebda66d9db617364e9c0ef5b2741290e2379866531ca4" + }, + { + "module": "WASmaxInGroupsSetSubjectResponseClientError", + "bundleSha256": "ed2f08bc12a1128a06ddafe25fc62e5d102593245a8b5e818fd8f2eedab3ffb9", + "start": 26419, + "end": 27130, + "sourceSha256": "b13f00ad281c8006568ebe8d992939686ce0ee849f4f38eef86ace0f556383e2" + }, + { + "module": "WASmaxInGroupsSetSubjectResponseServerError", + "bundleSha256": "ed2f08bc12a1128a06ddafe25fc62e5d102593245a8b5e818fd8f2eedab3ffb9", + "start": 27132, + "end": 27490, + "sourceSha256": "4acd4e98757ade26c0f22e539c73cb0000dedbfe326342ad6a339527039b0672" + }, + { + "module": "WASmaxParseReference", + "bundleSha256": "657a32007737e50b0cb0292ce9c364e21e48291b558129753b25e3ec2a4c13b7", + "start": 174246, + "end": 175353, + "sourceSha256": "e27311d25f3d328658355ab0c619b71b007d68ee2e83a7c5ae508a86e05e8aee" + }, + { + "module": "WASmaxParseUtils", + "bundleSha256": "657a32007737e50b0cb0292ce9c364e21e48291b558129753b25e3ec2a4c13b7", + "start": 165556, + "end": 172278, + "sourceSha256": "a2bbd76df3740e48f0f5c979c5ecc94d30217e726c7b42f2b23490161bcc42e6" + } + ] +} diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddClientErrors.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddClientErrors.js new file mode 100644 index 00000000..5276e937 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddClientErrors.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddClientErrors",["WAResultOrError","WASmaxInGroupsIQErrorAlreadyExistsMixin","WASmaxInGroupsIQErrorBadRequestMixin","WASmaxInGroupsIQErrorConflictMixin","WASmaxInGroupsIQErrorFallbackClientMixin","WASmaxInGroupsIQErrorForbiddenMixin","WASmaxInGroupsIQErrorGoneMixin","WASmaxInGroupsIQErrorItemNotFoundMixin","WASmaxInGroupsIQErrorLockedMixin","WASmaxInGroupsIQErrorNotAllowedMixin","WASmaxInGroupsIQErrorNotAuthorizedMixin","WASmaxInGroupsIQErrorParentLinkedGroupsParticipantsResourceLimitMixin","WASmaxInGroupsIQErrorResourceConstraintMixin","WASmaxInGroupsIQErrorResourceLimitMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxInGroupsIQErrorAlreadyExistsMixin").parseIQErrorAlreadyExistsMixin(e);if(t.success)return o("WAResultOrError").makeResult({name:"IQErrorAlreadyExists",value:t.value});var n=o("WASmaxInGroupsIQErrorBadRequestMixin").parseIQErrorBadRequestMixin(e);if(n.success)return o("WAResultOrError").makeResult({name:"IQErrorBadRequest",value:n.value});var r=o("WASmaxInGroupsIQErrorNotAuthorizedMixin").parseIQErrorNotAuthorizedMixin(e);if(r.success)return o("WAResultOrError").makeResult({name:"IQErrorNotAuthorized",value:r.value});var a=o("WASmaxInGroupsIQErrorForbiddenMixin").parseIQErrorForbiddenMixin(e);if(a.success)return o("WAResultOrError").makeResult({name:"IQErrorForbidden",value:a.value});var i=o("WASmaxInGroupsIQErrorItemNotFoundMixin").parseIQErrorItemNotFoundMixin(e);if(i.success)return o("WAResultOrError").makeResult({name:"IQErrorItemNotFound",value:i.value});var l=o("WASmaxInGroupsIQErrorConflictMixin").parseIQErrorConflictMixin(e);if(l.success)return o("WAResultOrError").makeResult({name:"IQErrorConflict",value:l.value});var s=o("WASmaxInGroupsIQErrorGoneMixin").parseIQErrorGoneMixin(e);if(s.success)return o("WAResultOrError").makeResult({name:"IQErrorGone",value:s.value});var u=o("WASmaxInGroupsIQErrorResourceLimitMixin").parseIQErrorResourceLimitMixin(e);if(u.success)return o("WAResultOrError").makeResult({name:"IQErrorResourceLimit",value:u.value});var c=o("WASmaxInGroupsIQErrorLockedMixin").parseIQErrorLockedMixin(e);if(c.success)return o("WAResultOrError").makeResult({name:"IQErrorLocked",value:c.value});var d=o("WASmaxInGroupsIQErrorResourceConstraintMixin").parseIQErrorResourceConstraintMixin(e);if(d.success)return o("WAResultOrError").makeResult({name:"IQErrorResourceConstraint",value:d.value});var m=o("WASmaxInGroupsIQErrorNotAllowedMixin").parseIQErrorNotAllowedMixin(e);if(m.success)return o("WAResultOrError").makeResult({name:"IQErrorNotAllowed",value:m.value});var p=o("WASmaxInGroupsIQErrorParentLinkedGroupsParticipantsResourceLimitMixin").parseIQErrorParentLinkedGroupsParticipantsResourceLimitMixin(e);if(p.success)return o("WAResultOrError").makeResult({name:"IQErrorParentLinkedGroupsParticipantsResourceLimit",value:p.value});var _=o("WASmaxInGroupsIQErrorFallbackClientMixin").parseIQErrorFallbackClientMixin(e);return _.success?o("WAResultOrError").makeResult({name:"IQErrorFallbackClient",value:_.value}):o("WASmaxParseUtils").errorMixinDisjunction(e,["IQErrorAlreadyExists","IQErrorBadRequest","IQErrorNotAuthorized","IQErrorForbidden","IQErrorItemNotFound","IQErrorConflict","IQErrorGone","IQErrorResourceLimit","IQErrorLocked","IQErrorResourceConstraint","IQErrorNotAllowed","IQErrorParentLinkedGroupsParticipantsResourceLimit","IQErrorFallbackClient"],[t,n,r,a,i,l,s,u,c,d,m,p,_])}l.parseAcceptGroupAddClientErrors=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseClientError.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseClientError.js new file mode 100644 index 00000000..db464e78 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseClientError.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddResponseClientError",["WAResultOrError","WASmaxInGroupsAcceptGroupAddClientErrors","WASmaxInGroupsIQErrorResponseMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseUtils").flattenedChildWithTag(e,"error");if(!r.success)return r;var a=o("WASmaxInGroupsIQErrorResponseMixin").parseIQErrorResponseMixin(e,t);if(!a.success)return a;var i=o("WASmaxInGroupsAcceptGroupAddClientErrors").parseAcceptGroupAddClientErrors(r.value);return i.success?o("WAResultOrError").makeResult(babelHelpers.extends({},a.value,{errorAcceptGroupAddClientErrors:i.value})):i}l.parseAcceptGroupAddResponseClientError=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseServerError.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseServerError.js new file mode 100644 index 00000000..596bd2b0 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsAcceptGroupAddResponseServerError.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsAcceptGroupAddResponseServerError",["WASmaxInGroupsBaseServerErrorMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxInGroupsBaseServerErrorMixin").parseBaseServerErrorMixin(e,t);return r.success,r}l.parseAcceptGroupAddResponseServerError=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorAlreadyExistsMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorAlreadyExistsMixin.js new file mode 100644 index 00000000..c8ffd4f9 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorAlreadyExistsMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorAlreadyExistsMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","already-exists");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",304);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorAlreadyExistsMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorBadRequestMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorBadRequestMixin.js new file mode 100644 index 00000000..b656b821 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorBadRequestMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorBadRequestMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","bad-request");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",400);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorBadRequestMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorFallbackClientMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorFallbackClientMixin.js new file mode 100644 index 00000000..baa00afb --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorFallbackClientMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorFallbackClientMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").attrString(e,"text");if(!n.success)return n;var r=o("WASmaxParseUtils").attrIntRange(e,"code",400,499);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorFallbackClientMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorFallbackServerMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorFallbackServerMixin.js new file mode 100644 index 00000000..0c4b94d3 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorFallbackServerMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorFallbackServerMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").attrString(e,"text");if(!n.success)return n;var r=o("WASmaxParseUtils").attrIntRange(e,"code",500,599);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorFallbackServerMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorInternalServerErrorMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorInternalServerErrorMixin.js new file mode 100644 index 00000000..6bd4c807 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorInternalServerErrorMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorInternalServerErrorMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","internal-server-error");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",500);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorInternalServerErrorMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorNotAcceptableMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorNotAcceptableMixin.js new file mode 100644 index 00000000..f1c2016b --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorNotAcceptableMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorNotAcceptableMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"field");if(!t.success)return t;var n=o("WASmaxParseUtils").attrString(e,"name");if(!n.success)return n;var r=o("WASmaxParseUtils").attrString(e,"reason");return r.success?o("WAResultOrError").makeResult({name:n.value,reason:r.value}):r}function s(t){var n=o("WASmaxParseUtils").assertTag(t,"error");if(!n.success)return n;var r=o("WASmaxParseUtils").optionalChildWithTag(t,"field",e);if(!r.success)return r;var a=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,t,"text","not-acceptable");if(!a.success)return a;var i=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,t,"code",406);return i.success?o("WAResultOrError").makeResult({text:a.value,code:i.value,field:r.value}):i}l.parseIQErrorNotAcceptableField=e,l.parseIQErrorNotAcceptableMixin=s}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorPartialServerErrorMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorPartialServerErrorMixin.js new file mode 100644 index 00000000..26e39dc5 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorPartialServerErrorMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorPartialServerErrorMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","partial-server-error");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",530);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorPartialServerErrorMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorResourceConstraintMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorResourceConstraintMixin.js new file mode 100644 index 00000000..b1bc3e4c --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorResourceConstraintMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorResourceConstraintMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","resource-constraint");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",500);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorResourceConstraintMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorResponseMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorResponseMixin.js new file mode 100644 index 00000000..c5d71a87 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorResponseMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorResponseMixin",["WAResultOrError","WASmaxParseReference","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseReference").attrStringFromReference(t,["id"]);if(!r.success)return r;var a=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"id",r.value);if(!a.success)return a;var i=o("WASmaxParseReference").attrStringFromReference(t,["to"]);if(!i.success)return i;var l=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"from",i.value);if(!l.success)return l;var s=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"type","error");return s.success?o("WAResultOrError").makeResult({type:s.value}):s}l.parseIQErrorResponseMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorServiceUnavailableMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorServiceUnavailableMixin.js new file mode 100644 index 00000000..9a98821b --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsIQErrorServiceUnavailableMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsIQErrorServiceUnavailableMixin",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"error");if(!t.success)return t;var n=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrString,e,"text","service-unavailable");if(!n.success)return n;var r=o("WASmaxParseUtils").literal(o("WASmaxParseUtils").attrInt,e,"code",503);return r.success?o("WAResultOrError").makeResult({text:n.value,code:r.value}):r}l.parseIQErrorServiceUnavailableMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsServerErrors.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsServerErrors.js new file mode 100644 index 00000000..ad19360e --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsServerErrors.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsServerErrors",["WAResultOrError","WASmaxInGroupsIQErrorFallbackServerMixin","WASmaxInGroupsIQErrorInternalServerErrorMixin","WASmaxInGroupsIQErrorPartialServerErrorMixin","WASmaxInGroupsIQErrorServiceUnavailableMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxInGroupsIQErrorInternalServerErrorMixin").parseIQErrorInternalServerErrorMixin(e);if(t.success)return o("WAResultOrError").makeResult({name:"IQErrorInternalServerError",value:t.value});var n=o("WASmaxInGroupsIQErrorServiceUnavailableMixin").parseIQErrorServiceUnavailableMixin(e);if(n.success)return o("WAResultOrError").makeResult({name:"IQErrorServiceUnavailable",value:n.value});var r=o("WASmaxInGroupsIQErrorPartialServerErrorMixin").parseIQErrorPartialServerErrorMixin(e);if(r.success)return o("WAResultOrError").makeResult({name:"IQErrorPartialServerError",value:r.value});var a=o("WASmaxInGroupsIQErrorFallbackServerMixin").parseIQErrorFallbackServerMixin(e);return a.success?o("WAResultOrError").makeResult({name:"IQErrorFallbackServer",value:a.value}):o("WASmaxParseUtils").errorMixinDisjunction(e,["IQErrorInternalServerError","IQErrorServiceUnavailable","IQErrorPartialServerError","IQErrorFallbackServer"],[t,n,r,a])}l.parseServerErrors=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectClientErrors.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectClientErrors.js new file mode 100644 index 00000000..4fb442a4 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectClientErrors.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsSetSubjectClientErrors",["WAResultOrError","WASmaxInGroupsIQErrorBadRequestMixin","WASmaxInGroupsIQErrorFallbackClientMixin","WASmaxInGroupsIQErrorForbiddenMixin","WASmaxInGroupsIQErrorItemNotFoundMixin","WASmaxInGroupsIQErrorLockedMixin","WASmaxInGroupsIQErrorNotAcceptableMixin","WASmaxInGroupsIQErrorNotAuthorizedMixin","WASmaxInGroupsIQErrorRateOverlimitMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxInGroupsIQErrorBadRequestMixin").parseIQErrorBadRequestMixin(e);if(t.success)return o("WAResultOrError").makeResult({name:"IQErrorBadRequest",value:t.value});var n=o("WASmaxInGroupsIQErrorItemNotFoundMixin").parseIQErrorItemNotFoundMixin(e);if(n.success)return o("WAResultOrError").makeResult({name:"IQErrorItemNotFound",value:n.value});var r=o("WASmaxInGroupsIQErrorNotAuthorizedMixin").parseIQErrorNotAuthorizedMixin(e);if(r.success)return o("WAResultOrError").makeResult({name:"IQErrorNotAuthorized",value:r.value});var a=o("WASmaxInGroupsIQErrorForbiddenMixin").parseIQErrorForbiddenMixin(e);if(a.success)return o("WAResultOrError").makeResult({name:"IQErrorForbidden",value:a.value});var i=o("WASmaxInGroupsIQErrorNotAcceptableMixin").parseIQErrorNotAcceptableMixin(e);if(i.success)return o("WAResultOrError").makeResult({name:"IQErrorNotAcceptable",value:i.value});var l=o("WASmaxInGroupsIQErrorLockedMixin").parseIQErrorLockedMixin(e);if(l.success)return o("WAResultOrError").makeResult({name:"IQErrorLocked",value:l.value});var s=o("WASmaxInGroupsIQErrorRateOverlimitMixin").parseIQErrorRateOverlimitMixin(e);if(s.success)return o("WAResultOrError").makeResult({name:"IQErrorRateOverlimit",value:s.value});var u=o("WASmaxInGroupsIQErrorFallbackClientMixin").parseIQErrorFallbackClientMixin(e);return u.success?o("WAResultOrError").makeResult({name:"IQErrorFallbackClient",value:u.value}):o("WASmaxParseUtils").errorMixinDisjunction(e,["IQErrorBadRequest","IQErrorItemNotFound","IQErrorNotAuthorized","IQErrorForbidden","IQErrorNotAcceptable","IQErrorLocked","IQErrorRateOverlimit","IQErrorFallbackClient"],[t,n,r,a,i,l,s,u])}l.parseSetSubjectClientErrors=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseClientError.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseClientError.js new file mode 100644 index 00000000..3a007da1 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseClientError.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsSetSubjectResponseClientError",["WAResultOrError","WASmaxInGroupsIQErrorResponseMixin","WASmaxInGroupsSetSubjectClientErrors","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseUtils").flattenedChildWithTag(e,"error");if(!r.success)return r;var a=o("WASmaxInGroupsIQErrorResponseMixin").parseIQErrorResponseMixin(e,t);if(!a.success)return a;var i=o("WASmaxInGroupsSetSubjectClientErrors").parseSetSubjectClientErrors(r.value);return i.success?o("WAResultOrError").makeResult(babelHelpers.extends({},a.value,{errorSetSubjectClientErrors:i.value})):i}l.parseSetSubjectResponseClientError=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseServerError.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseServerError.js new file mode 100644 index 00000000..89f5986f --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsSetSubjectResponseServerError.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsSetSubjectResponseServerError",["WASmaxInGroupsBaseServerErrorMixin","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxInGroupsBaseServerErrorMixin").parseBaseServerErrorMixin(e,t);return r.success,r}l.parseSetSubjectResponseServerError=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxParseReference.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxParseReference.js new file mode 100644 index 00000000..4db455e8 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxParseReference.js @@ -0,0 +1 @@ +__d("WASmaxParseReference",["WAHasProperty","WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){"use strict";function e(e,t,n,r,o){var a=m(t,n);if(!a.success)return _(a);var i=e(a.value,n[n.length-1],r,o);return i.success?i:_(i)}function s(t,n,r,a,i){return p(n,r)?e(t,n,r,a,i):o("WASmaxParseUtils").voidSuccess}function u(t,n){return e(o("WASmaxParseUtils").attrString,t,n)}function c(e,t){return p(e,t)?u(e,t):o("WASmaxParseUtils").voidSuccess}function d(e,t){var n=m(e,t);if(!n.success)return _(n);var r=o("WASmaxParseUtils").contentString(n.value);return r.success?r:_(r)}function m(e,t){for(var n=t.length,r=e,a=0;a"):e}function u(e,t){return B(e,t)?W(e,e.attrs[t]):O(e,'to have attribute "'+t+'"')}function c(e,t,n,r){var a=u(e,t);if(!a.success)return a;var i=n(a.value);return i!=null?o("WAResultOrError").makeResult(i):O(e,'to have "'+t+'"={'+r+'}, but instead has "'+a.value+'"')}function d(e,t,n){var r=P(e);if(!r.success)return r;var a=t(r.value);return a!=null?o("WAResultOrError").makeResult(a):O(e,"to have "+n+' content, but instead has "'+r.value+'"')}function m(t,n,r){var o=u(t,n);if(o.success){if(o.value!==r)return O(t,'to have "'+n+'"="'+r+'", but instead has "'+o.value+'"')}else return o;return e}function p(e,t){return c(e,t,o("WAStanzaUtils").toStanzaId,"stanzaID")}function _(e,t){return c(e,t,o("WAStanzaUtils").validateCallId,"callID")}function f(e,t){return c(e,t,q,"integer")}function g(e,t,n,r){var a=f(e,t);if(!a.success)return a;var i=a.value;return n!==void 0&&ir?O(e,'to have "'+t+'"={at most '+r+"} but has value "+i):o("WAResultOrError").makeResult(i)}function h(e){var t=e.content;return t instanceof Uint8Array?O(e,"to have children"):o("WAResultOrError").makeResult(t)}function y(t,n){var r=h(t);if(!r.success)return r;var a=r.value;if(a==null)return e;for(var i=null,l=0;l, but found more than 1");i=s}}return o("WAResultOrError").makeResult(i)}function C(t,n,r){var o=y(t,n);return o.success?o.value==null?e:r(o.value):o}function b(e,t,n){var r=C(e,t,n);if(!r.success)return r;var a=r.value;return a==null?O(e,"to have 1 child <"+t+">, but found 0"):o("WAResultOrError").makeResult(a)}function v(e,t){var n=y(e,t);return n.success?n.value==null?O(e,"to have 1 child <"+t+">, but found 0"):o("WAResultOrError").makeResult(n.value):n}function S(e,t,n,r,a){var i=U(e,t,n,r);if(!i.success)return i;for(var l=[],s=0;sn?O(e,"to have binary content at most "+n+" bytes but has "+a+" bytes"):o("WAResultOrError").makeResult(r)}function A(e){return d(e,q,"integer")}function F(e){return e}function O(e,t){return o("WAResultOrError").makeError("expected <"+e.tag+">: "+t)}function B(e,t){return r("WAHasProperty")(e.attrs,t)}function W(e,t){return t instanceof o("WAWapJid").WapJid?o("WAResultOrError").makeResult(t.toString()):typeof t=="string"?o("WAResultOrError").makeResult(t):O(e,"decodeAsString: attribute is "+typeof t+" not a string: "+String(t))}function q(e){var t=parseInt(e,10);return Number.isNaN(t)?null:t}function U(e,t,n,r){var a=h(e);if(!a.success)return a;var i=a.value;if(i==null)return n!==0?O(e,"to have at least "+n+" <"+t+"> children, but found 0"):o("WAResultOrError").makeResult([]);for(var l=[],s=0;s children, but found "+c):c>r?O(e,"to have at most "+r+" <"+t+"> children, but found "+c):o("WAResultOrError").makeResult(l)}function V(e,t,n){var r=t.map(function(e,t){return e+": "+n[t].error});return O(e,["to match any of following mixins: "+t.join(", ")+", but all mixins failed."].concat(r).join(" "))}l.voidSuccess=e,l.assertTag=s,l.attrString=u,l.attrValidate=c,l.assertAttr=m,l.attrStanzaId=p,l.attrCallId=_,l.attrInt=f,l.attrIntRange=g,l.maybeChildren=h,l.optionalChild=y,l.optionalChildWithTag=C,l.childWithTag=b,l.flattenedChildWithTag=v,l.mapChildrenWithTag=S,l.countChildrenWithTag=R,l.mapHomogeneousChildrenWithTag=L,l.countHomogeneousChildrenWithTag=E,l.optional=k,l.optionalLiteral=I,l.literal=T,l.literalContent=D,l.attrStringEnum=x,l.contentStringEnum=$,l.contentString=P,l.contentBytes=N,l.contentLiteralBytes=M,l.contentBytesRange=w,l.contentInt=A,l.identity=F,l.errorMessage=O,l.errorMixinDisjunction=V}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/provenance.json b/tests/conformance/compiled/sources/2.3000.1047483476/provenance.json new file mode 100644 index 00000000..1cb17b9f --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/provenance.json @@ -0,0 +1,141 @@ +{ + "waVersion": "2.3000.1047483476", + "setHash": "05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b", + "bundleCount": 579, + "bundleBytes": 75824128, + "modules": [ + { + "module": "WASmaxInGroupsAcceptGroupAddClientErrors", + "bundleSha256": "34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2", + "start": 19, + "end": 3490, + "sourceSha256": "0bf34d35f47688d83e1703ff785f033e74974a5bb6b27fcc13dfd9a202b10604" + }, + { + "module": "WASmaxInGroupsAcceptGroupAddResponseClientError", + "bundleSha256": "34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2", + "start": 3492, + "end": 4227, + "sourceSha256": "0a34c41c5e0bdc020ddac5ea1a75ea3f957803f66651a477f96e2234447c80a4" + }, + { + "module": "WASmaxInGroupsAcceptGroupAddResponseServerError", + "bundleSha256": "34859849724a114869fd78e4479a6c412b4ab3ec36c3da2985d8ec4ecb75d5d2", + "start": 4730, + "end": 5096, + "sourceSha256": "0786f985a20352044f3f00cec811911fce0f002f4dedafdf3b37e1cd087cbab6" + }, + { + "module": "WASmaxInGroupsIQErrorAlreadyExistsMixin", + "bundleSha256": "7c7be0378ce2e7ff49b9f5909cf8a7d283000af82cc43bc071c2df3541ec5d56", + "start": 74970, + "end": 75485, + "sourceSha256": "0c207f6532b9d3ab012f556fd7725de77bfdd36289665e3b8e013f31f2c42f6d" + }, + { + "module": "WASmaxInGroupsIQErrorBadRequestMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 29559, + "end": 30065, + "sourceSha256": "fc526eb6a0bb5f2d142d2215e61946163ffc0939eb67ea42a64f0d5aecf4e710" + }, + { + "module": "WASmaxInGroupsIQErrorFallbackClientMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 85725, + "end": 86174, + "sourceSha256": "2cf97c340ea2ebfac9c4c6b94933ebc71a7874ee249ac33ae68d920a60e5813f" + }, + { + "module": "WASmaxInGroupsIQErrorFallbackServerMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 32844, + "end": 33293, + "sourceSha256": "c6ddb72b768a67e8fdaee58b037b2de66c896c4ecb95d67d7d8bed6a64af0bf7" + }, + { + "module": "WASmaxInGroupsIQErrorInternalServerErrorMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 33295, + "end": 33829, + "sourceSha256": "3f728f0545d258df831a6c1a01f9f39c3040c3a4a04b931ee509ccbbc080b4a2" + }, + { + "module": "WASmaxInGroupsIQErrorNotAcceptableMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 88701, + "end": 89641, + "sourceSha256": "88e536fc9040f47de39a3f98ed5f299e9bffb37baa0590ec43ee619061802d56" + }, + { + "module": "WASmaxInGroupsIQErrorPartialServerErrorMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 33831, + "end": 34362, + "sourceSha256": "175917f45fe458ff9b1c229cb025661a7c0fac5664dc0ce983e5f8df44b5da67" + }, + { + "module": "WASmaxInGroupsIQErrorResourceConstraintMixin", + "bundleSha256": "7783a694b3a8bc3a379585817de0e477c75729c73b21adbffbf060f5f0ac2860", + "start": 623, + "end": 1153, + "sourceSha256": "00381d7ab0263c85d58b2c01f4f3bf446cc127c51322bbbd04fa3baf07fc3317" + }, + { + "module": "WASmaxInGroupsIQErrorResponseMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 31289, + "end": 32087, + "sourceSha256": "867cb414eafa8d1ddfec085c39696220e7fa07894dd27b8404a9c2ff7f3408e6" + }, + { + "module": "WASmaxInGroupsIQErrorServiceUnavailableMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 34364, + "end": 34894, + "sourceSha256": "6d1e9bb5f7a3f4afb0a1bc90f2f6e1a94f96c305d9813540e9a2cf917196b713" + }, + { + "module": "WASmaxInGroupsServerErrors", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 34896, + "end": 36163, + "sourceSha256": "4ff5d583d148680474bb75b36a6c8fc431d303ddd952e49b94d02d93a3a480d8" + }, + { + "module": "WASmaxInGroupsSetSubjectClientErrors", + "bundleSha256": "65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55", + "start": 24289, + "end": 26417, + "sourceSha256": "0ee8d5ffc0942026ba3ebda66d9db617364e9c0ef5b2741290e2379866531ca4" + }, + { + "module": "WASmaxInGroupsSetSubjectResponseClientError", + "bundleSha256": "65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55", + "start": 26419, + "end": 27130, + "sourceSha256": "b13f00ad281c8006568ebe8d992939686ce0ee849f4f38eef86ace0f556383e2" + }, + { + "module": "WASmaxInGroupsSetSubjectResponseServerError", + "bundleSha256": "65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55", + "start": 27132, + "end": 27490, + "sourceSha256": "4acd4e98757ade26c0f22e539c73cb0000dedbfe326342ad6a339527039b0672" + }, + { + "module": "WASmaxParseReference", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 2265026, + "end": 2266133, + "sourceSha256": "e27311d25f3d328658355ab0c619b71b007d68ee2e83a7c5ae508a86e05e8aee" + }, + { + "module": "WASmaxParseUtils", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 2256336, + "end": 2263058, + "sourceSha256": "a2bbd76df3740e48f0f5c979c5ecc94d30217e726c7b42f2b23490161bcc42e6" + } + ] +} From 4d1bed46d118a51cbebe93126cf3d5e5f7d7f37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:27:19 +0000 Subject: [PATCH 14/52] feat: trace counted snapshot gaps to checked AST source --- crates/wa-transform/src/lib.rs | 33 ++++++++++++ crates/wa-wam/src/callsites.rs | 3 ++ crates/wa-wam/src/lib.rs | 83 ++++++++++++++++++++++++++++- crates/whatspec/src/main.rs | 4 ++ crates/whatspec/src/quality_gaps.rs | 56 +++++++++++++++++++ crates/whatspec/src/source_index.rs | 28 +++++++--- 6 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 crates/whatspec/src/quality_gaps.rs diff --git a/crates/wa-transform/src/lib.rs b/crates/wa-transform/src/lib.rs index dac24317..525bdcac 100644 --- a/crates/wa-transform/src/lib.rs +++ b/crates/wa-transform/src/lib.rs @@ -45,6 +45,28 @@ pub fn extract_module_definitions(code: &str) -> Vec { visitor.modules } +/// Extract definitions only when the complete input parses without recovery. +/// Useful for provenance audits where an absent definition must not be confused +/// with a parse error. The existing extraction API keeps its recovery behavior. +pub fn extract_module_definitions_checked( + code: &str, +) -> Result, Vec> { + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, code, SourceType::cjs()).parse(); + if !ret.errors.is_empty() || ret.panicked { + let mut errors: Vec = ret.errors.iter().map(ToString::to_string).collect(); + if errors.is_empty() { + errors.push("parser did not complete".to_string()); + } + return Err(errors); + } + let mut visitor = ModuleVisitor { + modules: Vec::new(), + }; + visitor.visit_program(&ret.program); + Ok(visitor.modules) +} + struct ModuleVisitor { modules: Vec, } @@ -107,6 +129,17 @@ fn parse_define(call: &CallExpression) -> Option { mod tests { use super::*; + #[test] + fn checked_module_index_rejects_parse_recovery() { + let valid = r#"__d("M",[],function(){return 1;});"#; + assert_eq!( + extract_module_definitions_checked(valid).unwrap(), + extract_module_definitions(valid) + ); + assert!(extract_module_definitions_checked(r#"__d("M",[],function(){"#).is_err()); + assert!(extract_module_definitions_checked("").unwrap().is_empty()); + } + #[test] fn empty_input() { assert!(extract_module_definitions("").is_empty()); diff --git a/crates/wa-wam/src/callsites.rs b/crates/wa-wam/src/callsites.rs index da9a38ae..024c8794 100644 --- a/crates/wa-wam/src/callsites.rs +++ b/crates/wa-wam/src/callsites.rs @@ -43,6 +43,8 @@ pub(crate) struct RawSite { pub unread_argument: Option<&'static str>, /// Position, so later writes can be attached to the nearest construction before them. pub start: u32, + /// End-exclusive construction span in the module slice. + pub end: u32, /// What the value is bound to, when it is bound at all. pub binding: Option, } @@ -339,6 +341,7 @@ impl<'a> Visit<'a> for SiteVisitor<'_> { partial, unread_argument, start: n.span.start, + end: n.span.end, binding: self.bindings.get(&n.span.start).cloned(), }); } diff --git a/crates/wa-wam/src/lib.rs b/crates/wa-wam/src/lib.rs index 77fffaef..f8bb782c 100644 --- a/crates/wa-wam/src/lib.rs +++ b/crates/wa-wam/src/lib.rs @@ -54,6 +54,20 @@ const CONSTRUCTION_NO_CATALOG_ENTRY: &str = "construction of an event with no ca /// rather than as a bare word. const UNREAD_ARGUMENT_PREFIX: &str = "unreadConstructionArgument"; +/// A counted WAM gap with a recoverable construction location. Kept out of the IR. +#[derive(Debug, Clone)] +pub struct WamGapSite { + pub module: String, + /// Module start in the source passed to the extractor. + pub module_start: usize, + /// Construction span relative to that module, end exclusive. + pub start: u32, + pub end: u32, + pub event_module: String, + pub event_export: String, + pub reason: String, +} + /// What the scan recovered and, more importantly, what it did not. #[derive(Debug, Default, Clone)] pub struct WamDiagnostics { @@ -102,6 +116,26 @@ pub fn extract_wam_from_modules( source: &str, module_defs: &[ModuleDefinition], wa_version: &str, +) -> (WamIr, WamDiagnostics) { + extract_with_gaps(source, module_defs, wa_version, None) +} + +/// Extract with an optional provenance sidecar, without changing the IR or diagnostics API. +pub fn extract_wam_with_gap_sites( + source: &str, + module_defs: &[ModuleDefinition], + wa_version: &str, +) -> (WamIr, WamDiagnostics, Vec) { + let mut gaps = Vec::new(); + let (ir, diag) = extract_with_gaps(source, module_defs, wa_version, Some(&mut gaps)); + (ir, diag, gaps) +} + +fn extract_with_gaps( + source: &str, + module_defs: &[ModuleDefinition], + wa_version: &str, + gap_sites: Option<&mut Vec>, ) -> (WamIr, WamDiagnostics) { let mut diag = WamDiagnostics::default(); @@ -176,7 +210,14 @@ pub fn extract_wam_from_modules( diag.private_stats_ids = private_stats_ids.len(); diag.constants = constants.len(); - collect_call_sites(source, module_defs, &mut events, &exports, &mut diag); + collect_call_sites( + source, + module_defs, + &mut events, + &exports, + &mut diag, + gap_sites, + ); // A call site can name an enum no field of any event or global is typed by. The // catalog is resolved after the sites for exactly this reason: an `enumMember` value // pointing at a module the document does not carry would be a dangling reference in @@ -286,6 +327,7 @@ fn collect_call_sites( events: &mut [WamEvent], exports: &BTreeMap<(String, String), usize>, diag: &mut WamDiagnostics, + mut gap_sites: Option<&mut Vec>, ) { // event module → the single event it defines, for the modules that define one. let mut sole: BTreeMap<&str, usize> = BTreeMap::new(); @@ -313,7 +355,19 @@ fn collect_call_sites( // Counted here rather than during the scan: 2648 module names are defined by // more than one bundle file, so a residue counted per copy would report one // construction as several and could trip the lint baseline on a duplicate. + let gap = |reason: String| WamGapSite { + module: m.name.clone(), + module_start: m.start, + start: raw.start, + end: raw.end, + event_module: raw.event_module.clone(), + event_export: raw.export.clone(), + reason, + }; if let Some(form) = raw.unread_argument { + if let Some(sites) = gap_sites.as_deref_mut() { + sites.push(gap(format!("{UNREAD_ARGUMENT_PREFIX}.{form}"))); + } *diag .drops_by_reason .entry(format!("{UNREAD_ARGUMENT_PREFIX}.{form}")) @@ -337,6 +391,9 @@ fn collect_call_sites( } }); let Some(index) = index else { + if let Some(sites) = gap_sites.as_deref_mut() { + sites.push(gap(CONSTRUCTION_NO_CATALOG_ENTRY.to_string())); + } // `WAWebWamCodegenWamEvent`'s `RawWamEvent` is the generic envelope: a // construction of a schema the catalog does not define, by design. *diag @@ -1049,3 +1106,27 @@ fn parse_enum_variants(obj: &ObjectExpression) -> Option> { #[cfg(test)] mod tests; + +#[cfg(test)] +mod gap_evidence_tests { + #[test] + fn sidecar_counts_and_byte_spans_match_existing_diagnostics() { + let module = r#"__d("Reporter",["WAWebRawWamEvent"],function(){new(o("WAWebRawWamEvent")).RawWamEvent(unknown);});"#; + let source = format!("// á\n{module}\n{module}"); + let defs = wa_transform::extract_module_definitions_checked(&source).unwrap(); + let (ir, diag, gaps) = super::extract_wam_with_gap_sites(&source, &defs, "test"); + let (regular_ir, regular_diag) = super::extract_wam_from_modules(&source, &defs, "test"); + assert_eq!(ir.events, regular_ir.events); + assert_eq!(diag.drops_by_reason, regular_diag.drops_by_reason); + assert_eq!(gaps.len(), 2); // Two reasons on one construction, not two bundle copies. + let mut counts = std::collections::BTreeMap::new(); + for gap in gaps { + *counts.entry(gap.reason).or_insert(0) += 1; + assert_eq!( + &source[gap.module_start + gap.start as usize..gap.module_start + gap.end as usize], + r#"new(o("WAWebRawWamEvent")).RawWamEvent(unknown)"# + ); + } + assert_eq!(counts, diag.drops_by_reason); + } +} diff --git a/crates/whatspec/src/main.rs b/crates/whatspec/src/main.rs index 432acebf..8698f0fe 100644 --- a/crates/whatspec/src/main.rs +++ b/crates/whatspec/src/main.rs @@ -3,6 +3,7 @@ //! schemas) to disk, ready to be committed — locally or from CI. mod contract_diff; +mod quality_gaps; mod source_index; use std::collections::BTreeSet; @@ -55,6 +56,7 @@ fn main() -> Result<()> { Some("mex-ids") => mex_ids(&args[1..]), Some("diff") => diff(&args[1..]), Some("source-index") => source_index::run(&args[1..]), + Some("quality-gaps") => quality_gaps::run(&args[1..]), Some("restore") => restore_cmd(&args[1..]), _ => { eprintln!("{}", usage()); @@ -156,6 +158,8 @@ fn usage() -> String { and prints version/count deltas and the namespaces/operations/actions added or removed.\n\n\ whatspec source-index [module ...]\n\n\ Writes a verified-bundle AST source locator index to stdout.\n\n\ + whatspec quality-gaps \n\n\ + Writes counted WAM gap locations from verified bundles to stdout.\n\n\ whatspec restore {FLAG_FROM_LOCK} ({FLAG_OUT} | {FLAG_CACHE} )\n \ [{FLAG_WASM}] [{FLAG_ARCHIVE} ] [{FLAG_REPO} ]\n\n\ Rebuilds the exact bundle set a `generated/` snapshot was built from — pulled from the\n\ diff --git a/crates/whatspec/src/quality_gaps.rs b/crates/whatspec/src/quality_gaps.rs new file mode 100644 index 00000000..6fea6dca --- /dev/null +++ b/crates/whatspec/src/quality_gaps.rs @@ -0,0 +1,56 @@ +//! Optional WAM gap evidence tied to a verified bundle set. +use anyhow::{Result, ensure}; +use serde_json::json; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; +use wa_store::lock::BundleLock; + +pub fn run(args: &[String]) -> Result<()> { + ensure!( + args.len() == 2, + "usage: whatspec quality-gaps " + ); + let lock: BundleLock = serde_json::from_slice(&std::fs::read(&args[0])?)?; + lock.verify_self_consistent().map_err(anyhow::Error::msg)?; + // Verifies UTF-8 and parses every locked bundle without recovery before using byte spans. + let index = + super::source_index::index(Path::new(&args[0]), Path::new(&args[1]), &BTreeSet::new())?; + let (source, identities) = super::read_local_bundles(Path::new(&args[1]))?; + let observed = BundleLock::new(&lock.wa_version, identities); + ensure!( + observed.set_hash == lock.set_hash && observed.bundle_count == lock.bundle_count, + "bundle set differs from lock" + ); + let defs = wa_transform::extract_module_definitions_checked(&source) + .map_err(|errors| anyhow::anyhow!("bundle set parse failed: {}", errors.join("; ")))?; + let by_start: BTreeMap<_, _> = defs.iter().map(|d| (d.start, d)).collect(); + let (_, diag, gaps) = wa_wam::extract_wam_with_gap_sites(&source, &defs, &lock.wa_version); + let mut sites = Vec::new(); + for gap in gaps { + let definition = by_start[&gap.module_start]; + let module = &source[definition.start..definition.end]; + let module_hash = wa_text::sha256_hex(module.as_bytes()); + let sources: Vec<_> = index["modules"][&gap.module] + .as_array() + .unwrap() + .iter() + .filter(|v| v["moduleSha256"] == module_hash) + .map(|v| json!({"bundleSha256":v["bundleSha256"],"start":v["start"],"end":v["end"]})) + .collect(); + ensure!( + !sources.is_empty(), + "construction module is absent from verified source index" + ); + sites.push(json!({"sources":sources,"module":gap.module,"moduleSha256":wa_text::sha256_hex(module.as_bytes()), + "start":gap.start,"end":gap.end,"eventModule":gap.event_module,"eventExport":gap.event_export,"reason":gap.reason, + "constructionSha256":wa_text::sha256_hex(&module.as_bytes()[gap.start as usize..gap.end as usize])})); + } + sites.sort_by_key(|v| serde_json::to_string(v).unwrap()); + println!( + "{}", + serde_json::to_string_pretty( + &json!({"qualityReportVersion":1,"waVersion":lock.wa_version,"setHash":lock.set_hash,"dropsByReason":diag.drops_by_reason,"wamGapSites":sites}) + )? + ); + Ok(()) +} diff --git a/crates/whatspec/src/source_index.rs b/crates/whatspec/src/source_index.rs index 87d1521c..f8cb2a22 100644 --- a/crates/whatspec/src/source_index.rs +++ b/crates/whatspec/src/source_index.rs @@ -6,8 +6,10 @@ use anyhow::{Context, Result, ensure}; use serde_json::{Value, json}; use wa_store::lock::{BundleId, BundleLock}; -fn modules(source: &str, hash: &str, selected: &BTreeSet<&str>) -> Vec<(String, Value)> { - wa_transform::extract_module_definitions(source).into_iter() +fn modules(source: &str, hash: &str, selected: &BTreeSet<&str>) -> Result> { + Ok(wa_transform::extract_module_definitions_checked(source) + .map_err(|errors| anyhow::anyhow!("bundle {hash}: {}", errors.join("; ")))? + .into_iter() .filter(|m| selected.is_empty() || selected.contains(m.name.as_str())) .map(|m| { let value = json!({"bundleSha256": hash, "start": m.start, "end": m.end, @@ -15,7 +17,7 @@ fn modules(source: &str, hash: &str, selected: &BTreeSet<&str>) -> Vec<(String, "factorySha256": wa_text::sha256_hex(&source.as_bytes()[m.factory_start..m.factory_end]), "dependencies": m.deps}); (m.name, value) - }).collect() + }).collect()) } pub fn index(lock_path: &Path, bundles: &Path, selected: &BTreeSet<&str>) -> Result { @@ -38,7 +40,7 @@ pub fn index(lock_path: &Path, bundles: &Path, selected: &BTreeSet<&str>) -> Res size: bytes.len() as u64, url: None, }); - for (name, definition) in modules(source, &hash, selected) { + for (name, definition) in modules(source, &hash, selected)? { definitions.entry(name).or_default().push(definition); } } @@ -58,9 +60,9 @@ pub fn index(lock_path: &Path, bundles: &Path, selected: &BTreeSet<&str>) -> Res Ok( json!({"sourceIndexVersion":1,"waVersion":lock.wa_version,"setHash":lock.set_hash, "bundleCount":lock.bundle_count,"modules":definitions, - "coverage":"recovered-definitions-only", + "coverage":"parsed-static-definitions", "limits":["Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", - "The existing AST extractor does not expose parse diagnostics. Absence from this index is not proof of absence upstream.", + "Every bundle parsed without recovery. Absence only establishes that this exact static module name was not defined in these inputs; it does not prove feature removal or a rename.", "All recovered occurrences are retained. Equal module names with different hashes need review.", "Module provenance locates code; field-level semantics may require following dependencies."]}), ) @@ -110,10 +112,22 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } + #[test] + fn malformed_source_cannot_claim_complete_coverage() { + assert!( + modules( + "__d(\"Before\",[],function(){}); function broken(", + "hash", + &BTreeSet::new() + ) + .is_err() + ); + } + #[test] fn ast_locations_ignore_strings_and_keep_duplicate_definitions() { let source = "// á UTF-8 prefix\nvar text = '__d(\"Fake\",[],function(){})'; __d(\"Real\",[\"Dep\"],function(){return 1;}); __d(\"Real\",[],function(){return 2;});"; - let result = modules(source, "bundle-hash", &BTreeSet::new()); + let result = modules(source, "bundle-hash", &BTreeSet::new()).unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].0, "Real"); let start = result[0].1["start"].as_u64().unwrap() as usize; From 58afb2ee9c513d42a5db058457fa689dfdfc0fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:31:00 +0000 Subject: [PATCH 15/52] fix: recover proven local object arguments in WAM constructions --- crates/wa-wam/src/callsites.rs | 10 + crates/wa-wam/src/callsites/arguments.rs | 205 +++++ crates/wa-wam/src/tests.rs | 99 +++ .../tests/fixtures/status-subtitle-new.js | 4 + .../tests/fixtures/status-subtitle-old.js | 4 + generated/manifest.json | 14 +- generated/wam/index.json | 703 +++++++++++++++++- 7 files changed, 1019 insertions(+), 20 deletions(-) create mode 100644 crates/wa-wam/src/callsites/arguments.rs create mode 100644 crates/wa-wam/tests/fixtures/status-subtitle-new.js create mode 100644 crates/wa-wam/tests/fixtures/status-subtitle-old.js diff --git a/crates/wa-wam/src/callsites.rs b/crates/wa-wam/src/callsites.rs index 024c8794..f38990f1 100644 --- a/crates/wa-wam/src/callsites.rs +++ b/crates/wa-wam/src/callsites.rs @@ -10,6 +10,8 @@ //! Deliberately not extracted: the condition a construction sits under. A call site is a //! place the client can emit the event, never a promise that it does. +mod arguments; + use std::collections::BTreeMap; use oxc_allocator::Allocator; @@ -88,6 +90,7 @@ pub(crate) fn scan_module(slice: &str) -> Vec { let alloc = Allocator::default(); let ret = parse_cjs(&alloc, slice); let aliases = crate::require_aliases(&ret.program); + let arguments = arguments::collect(&ret.program, &aliases); let mut v = SiteVisitor { bindings: BTreeMap::new(), sites: Vec::new(), @@ -97,6 +100,7 @@ pub(crate) fn scan_module(slice: &str) -> Vec { names: std::collections::BTreeSet::new(), }], aliases: &aliases, + arguments: &arguments, }; for stmt in &ret.program.body { v.visit_statement(stmt); @@ -162,6 +166,7 @@ struct SiteVisitor<'m> { /// Locals standing for a `o("Module")` require, so an enum member written through /// one resolves the same way a field type written through one does. aliases: &'m RequireAliases, + arguments: &'m BTreeMap, } /// One scope and the names it introduces. @@ -326,6 +331,11 @@ impl<'a> Visit<'a> for SiteVisitor<'_> { match n.arguments.first().and_then(wa_oxc::arg_expr) { // `new (…)(…)` with no argument: a real, complete field set of zero. None => {} + Some(arg) if self.arguments.contains_key(&arg.span().start) => { + let recovered = &self.arguments[&arg.span().start]; + fields = recovered.fields.clone(); + partial = recovered.partial; + } Some(arg) => read_argument( arg, &mut fields, diff --git a/crates/wa-wam/src/callsites/arguments.rs b/crates/wa-wam/src/callsites/arguments.rs new file mode 100644 index 00000000..4b0d2984 --- /dev/null +++ b/crates/wa-wam/src/callsites/arguments.rs @@ -0,0 +1,205 @@ +//! Recover direct local object arguments only when all uses are known reads. +//! No general alias analysis: mutation, rebinding, escape, conditional initialization, +//! duplicate bindings, and references before initialization leave the argument unread. +use crate::RequireAliases; +use oxc_ast::ast::*; +use oxc_ast_visit::{Visit, walk}; +use oxc_syntax::scope::ScopeFlags; +use std::collections::{BTreeMap, BTreeSet}; +use wa_ir::{WamCallSiteValue, WamFieldWrite}; + +#[derive(Clone)] +pub(super) struct Fields { + pub fields: Vec<(String, WamFieldWrite, Option)>, + pub partial: bool, +} + +type Key = (u32, String); +struct Candidate { + end: u32, + value: Fields, + valid: bool, +} +struct Frame { + start: u32, + names: BTreeSet, +} + +#[derive(Default)] +struct Locals { + names: BTreeMap, +} +impl<'a> Visit<'a> for Locals { + fn visit_binding_identifier(&mut self, id: &BindingIdentifier<'a>) { + *self.names.entry(id.name.to_string()).or_default() += 1; + } + fn visit_function(&mut self, f: &Function<'a>, _: ScopeFlags) { + // A declaration binds its name here, but its parameters/body belong to itself. + if f.is_function_declaration() + && let Some(id) = &f.id + { + self.visit_binding_identifier(id); + } + } + fn visit_arrow_function_expression(&mut self, _: &ArrowFunctionExpression<'a>) {} +} + +struct Collector<'b> { + aliases: &'b RequireAliases, + frames: Vec, + candidates: BTreeMap, + references: Vec<(u32, u32, Key)>, + allowed: BTreeSet, +} + +impl Collector<'_> { + fn enter<'a>( + &mut self, + start: u32, + params: &FormalParameters<'a>, + body: Option<&FunctionBody<'a>>, + ) { + let mut locals = Locals::default(); + locals.visit_formal_parameters(params); + if let Some(body) = body { + locals.visit_function_body(body); + } + if let Some(body) = body { + for stmt in &body.statements { + let Statement::VariableDeclaration(decl) = stmt else { + continue; + }; + for binding in &decl.declarations { + let Some(name) = binding.id.get_identifier_name() else { + continue; + }; + if locals.names.get(name.as_str()) != Some(&1) { + continue; + } + let Some(init) = binding + .init + .as_ref() + .filter(|v| wa_oxc::as_object(v).is_some()) + else { + continue; + }; + let mut fields = Vec::new(); + let mut partial = false; + let mut unread = None; + super::read_argument( + init, + &mut fields, + &mut partial, + &mut unread, + self.aliases, + ); + self.candidates.insert( + (start, name.to_string()), + Candidate { + end: binding.span.end, + value: Fields { fields, partial }, + valid: true, + }, + ); + } + } + } + self.frames.push(Frame { + start, + names: locals.names.into_keys().collect(), + }); + } + + fn allow_read(&mut self, arg: &Expression<'_>) { + if let Expression::Identifier(id) = arg { + self.allowed.insert(id.span.start); + } else if let Some(call) = wa_oxc::as_call(arg) + && super::is_object_merge(call) + // extends writes its first argument. Only a fresh literal target is safe. + && call.arguments.first().and_then(wa_oxc::arg_expr).is_some_and(|v| wa_oxc::as_object(v).is_some()) + { + for arg in call.arguments.iter().skip(1).filter_map(wa_oxc::arg_expr) { + if let Expression::Identifier(id) = arg { + self.allowed.insert(id.span.start); + } + } + } + } +} + +impl<'a> Visit<'a> for Collector<'_> { + fn visit_function(&mut self, f: &Function<'a>, flags: ScopeFlags) { + self.enter(f.span.start, &f.params, f.body.as_deref()); + walk::walk_function(self, f, flags); + self.frames.pop(); + } + fn visit_arrow_function_expression(&mut self, f: &ArrowFunctionExpression<'a>) { + self.enter(f.span.start, &f.params, Some(&f.body)); + walk::walk_arrow_function_expression(self, f); + self.frames.pop(); + } + fn visit_identifier_reference(&mut self, id: &IdentifierReference<'a>) { + if let Some(owner) = self + .frames + .iter() + .rev() + .find(|f| f.names.contains(id.name.as_str())) + { + self.references.push(( + id.span.start, + self.frames.last().map_or(0, |f| f.start), + (owner.start, id.name.to_string()), + )); + } + } + fn visit_new_expression(&mut self, n: &NewExpression<'a>) { + if super::wam_event_callee(&n.callee, self.aliases).is_some() + && let Some(arg) = n.arguments.first().and_then(wa_oxc::arg_expr) + { + self.allow_read(arg); + } + walk::walk_new_expression(self, n); + } + fn visit_with_statement(&mut self, stmt: &WithStatement<'a>) { + for candidate in self.candidates.values_mut() { + candidate.valid = false; + } + walk::walk_with_statement(self, stmt); + } + fn visit_call_expression(&mut self, c: &CallExpression<'a>) { + // Direct eval could mutate any visible binding without an AST reference. + if wa_oxc::as_identifier(&c.callee) == Some("eval") { + for candidate in self.candidates.values_mut() { + candidate.valid = false; + } + } + walk::walk_call_expression(self, c); + } +} + +pub(super) fn collect(program: &Program<'_>, aliases: &RequireAliases) -> BTreeMap { + let mut scan = Collector { + aliases, + frames: Vec::new(), + candidates: BTreeMap::new(), + references: Vec::new(), + allowed: BTreeSet::new(), + }; + scan.visit_program(program); + for (position, _, key) in &scan.references { + if let Some(candidate) = scan.candidates.get_mut(key) + && (!scan.allowed.contains(position) || *position < candidate.end) + { + candidate.valid = false; + } + } + scan.references + .into_iter() + .filter_map(|(position, scope, key)| { + let candidate = scan.candidates.get(&key)?; + // A closure may run before an outer initializer. Keep that case unresolved. + (candidate.valid && scope == key.0 && scan.allowed.contains(&position)) + .then(|| (position, candidate.value.clone())) + }) + .collect() +} diff --git a/crates/wa-wam/src/tests.rs b/crates/wa-wam/src/tests.rs index fe9225cd..b1972c94 100644 --- a/crates/wa-wam/src/tests.rs +++ b/crates/wa-wam/src/tests.rs @@ -732,3 +732,102 @@ __d("WAWebWamGlobals",["WAWebWamCodegenUtils"],(function(t,n,r,o,a,i,l){ Some(&2) ); } + +const LOCAL_ARGUMENT_CATALOG: &str = r#"__d("WAWebMessageSendWamEvent",["WAWebWamCodegenUtils"],function(t,n,r,o,a,i,l){var e=o("WAWebWamCodegenUtils");l.MessageSendWamEvent=e.defineEvents({MessageSend:[5,{retryCount:[1,e.TYPES.INTEGER],deviceCount:[2,e.TYPES.INTEGER]},[1,1,1]]},{MessageSend:[]});});"#; + +#[test] +fn object_argument_refactor_keeps_constructor_fields() { + // WAWebStatusSubtitle.react: inline fields moved into a function-local object. + let inline = r#"function send() { new(o("WAWebMessageSendWamEvent")).MessageSendWamEvent({retryCount: 3}); }"#; + let local = r#"function send(flag) { var fields = {retryCount: 3}; if (flag) { new(o("WAWebMessageSendWamEvent")).MessageSendWamEvent(fields); return; } later(function(){new(o("WAWebMessageSendWamEvent")).MessageSendWamEvent(babelHelpers.extends({},fields,{deviceCount: 1}));}); }"#; + let wrap = |body: &str| { + format!( + "{LOCAL_ARGUMENT_CATALOG}__d(\"Reporter\",[\"WAWebMessageSendWamEvent\"],function(){{{body}}});" + ) + }; + let (before, _) = run_full(&wrap(inline)); + let (after, diag) = run_full(&wrap(local)); + let before = before + .events + .iter() + .find(|e| e.name == "MessageSend") + .unwrap(); + let after = after + .events + .iter() + .find(|e| e.name == "MessageSend") + .unwrap(); + assert!( + after + .call_sites + .iter() + .any(|site| !site.partial && site.fields == before.call_sites[0].fields) + ); + assert!( + !diag + .drops_by_reason + .contains_key("unreadConstructionArgument.identifier") + ); + // The closure's timing and inherited fields are intentionally not inferred. + assert!(after.call_sites.iter().any(|site| site.partial)); +} + +#[test] +fn mutable_escaped_shadowed_and_conditionally_initialized_arguments_stay_partial() { + for body in [ + "function send(){var fields={retryCount:3}; fields.retryCount=4; emit(); new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", + "function send(){var fields={retryCount:3}; mutate(fields); new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", + "function send(){var fields={retryCount:3}; var alias=fields; alias.retryCount=4; new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", + "function send(flag){if(flag){var fields={retryCount:3};} new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", + "function send(){new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields); var fields={retryCount:3};}", + "function send(){var fields={retryCount:3}; function inner(){new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields); var fields;} inner();}", + "function send(){var fields={retryCount:3}; fields={deviceCount:1}; new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", + "function send(){var fields={retryCount:3}; function inner(){new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);} inner();}", + "function send(){var fields={retryCount:3}; eval('fields={}'); new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", + ] { + let (ir, _) = run_full(&format!( + "{LOCAL_ARGUMENT_CATALOG}__d(\"Reporter\",[\"WAWebMessageSendWamEvent\"],function(){{{body}}});" + )); + let ev = ir.events.iter().find(|e| e.name == "MessageSend").unwrap(); + assert_eq!(ev.call_sites.len(), 1, "{body}"); + assert!(ev.call_sites[0].partial, "{body}"); + assert!(ev.call_sites[0].fields.is_empty(), "{body}"); + } +} + +#[test] +fn pinned_status_subtitle_refactor_preserves_attribution_fields() { + let (old, _) = run_full(include_str!("../tests/fixtures/status-subtitle-old.js")); + let (new, _) = run_full(include_str!("../tests/fixtures/status-subtitle-new.js")); + let event = |ir: &WamIr| { + ir.events + .iter() + .find(|e| e.name == "StatusViewerAction") + .unwrap() + .clone() + }; + let old = event(&old); + let new = event(&new); + let fields = |site: &wa_ir::WamCallSite| { + site.fields + .iter() + .map(|f| f.name.clone()) + .collect::>() + }; + let expected = std::collections::BTreeSet::from([ + "attributionType".to_string(), + "statusCategory".to_string(), + "viewerActionType".to_string(), + ]); + assert!( + old.call_sites + .iter() + .any(|s| !s.partial && fields(s) == expected) + ); + assert!( + new.call_sites + .iter() + .any(|s| !s.partial && fields(s) == expected) + ); + assert!(new.call_sites.iter().any(|s| s.partial)); +} diff --git a/crates/wa-wam/tests/fixtures/status-subtitle-new.js b/crates/wa-wam/tests/fixtures/status-subtitle-new.js new file mode 100644 index 00000000..b73ddfd3 --- /dev/null +++ b/crates/wa-wam/tests/fixtures/status-subtitle-new.js @@ -0,0 +1,4 @@ +// {"waVersion": "2.3000.1047483476", "bundleSha256": "7a224a1f4e67acb7e519b9676abfa86806e50426e7ed4a9f76e517536fe279ca", "start": 301920, "end": 302952} +__d("WAWebStatusViewerActionWamEvent",["WAWebWamCodegenUtils","WAWebWamEnumInlineVideoType","WAWebWamEnumStatusCategory","WAWebWamEnumStatusViewActionType","WAWebWamEnumTopBarAttributionType","WAWebWamEnumUrlStatusClicked","WAWebWamEnumUrlStatusType"],(function(t,n,r,o,a,i,l){var e,s=(e=o("WAWebWamCodegenUtils")).defineEvents({StatusViewerAction:[6692,{attributionType:[1,o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE],attributionTypes:[3,e.TYPES.STRING],externalSourceDomainType:[5,o("WAWebWamEnumInlineVideoType").INLINE_VIDEO_TYPE],psaCampaignId:[11,e.TYPES.STRING],statusCategory:[4,o("WAWebWamEnumStatusCategory").STATUS_CATEGORY],statusId:[12,e.TYPES.STRING],statusViewerSessionId:[13,e.TYPES.INTEGER],urlStatusClicked:[6,o("WAWebWamEnumUrlStatusClicked").URL_STATUS_CLICKED],urlStatusType:[7,o("WAWebWamEnumUrlStatusType").URL_STATUS_TYPE],viewerActionType:[2,o("WAWebWamEnumStatusViewActionType").STATUS_VIEW_ACTION_TYPE]},[1,1,1],"regular"]},{StatusViewerAction:[]});l.StatusViewerActionWamEvent=s}),98); +// {"waVersion": "2.3000.1047483476", "bundleSha256": "7a224a1f4e67acb7e519b9676abfa86806e50426e7ed4a9f76e517536fe279ca", "start": 302954, "end": 315755} +__d("WAWebStatusSubtitle.react",["fbt","invariant","WALogger","WAWebClock","WAWebContactCollection","WAWebContactGetters","WAWebExternalLink.react","WAWebFlex.react","WAWebFrontendContactGetters","WAWebMentionsRefreshedIcon.react","WAWebMessageAIContentLabel.react","WAWebMsgGetters","WAWebMusicGatingUtils","WAWebMusicStatusAttribution.react","WAWebNewsletterAiContentInfoModalOpener","WAWebNewsletterAiContentInfoModalTypes","WAWebNewsletterGatingUtils","WAWebNewsletterMetadataCollection","WAWebProtobufsStatusAttributions.pb","WAWebStatusCloseFriendsDialog.react","WAWebStatusExternalShareUtils","WAWebStatusGatingUtils","WAWebStatusLoggingUtils","WAWebStatusNavigateToNewsletterUtils","WAWebStatusSubtitleAttribution.react","WAWebStatusViewerActionWamEvent","WAWebStopEvent","WAWebWamEnumStatusCategory","WAWebWamEnumStatusViewActionType","WAWebWamEnumTopBarAttributionType","WDSIconIcRepeat.react","WDSIconWdsIcAiContent.react","WDSIconWdsIcChannels.react","WDSText.react","asyncToGeneratorRuntime","react","react-compiler-runtime","stylex","useWAWebMsgValues"],(function(t,n,r,o,a,i,l,s,u){var e,c,d,m=d||(d=o("react")),p=d,_=p.useEffect,f=p.useRef,g=p.useState,h={subtitleRow:{overflowX:"x6ikm8r",overflowY:"x10wlt62",minWidth:"xeuugli",$$css:!0}},y=3e3,C=3;function b(e){return e==="music"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.MUSIC:e==="mention"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.MENTION:e==="groupStatus"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.GROUP_STATUS:e==="closeFriends"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.CLOSE_SHARING:e==="reshare"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.RESHARED_STATUS:e==="externalShare"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.CROSSPOSTING:e==="newsletterStatus"||e==="viewChannel"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.CHANNEL_STATUS:e==="aiContent"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.AI_CREATED:e==="paidPartnership"?u(!1,"paidPartnership attribution is not tappable"):(function(){throw Error("Match: No case succesfully matched. Make exhaustive or add a wildcard case using '_'. Argument: "+e)})()}function v(e,t){return e?o("WAWebWamEnumStatusCategory").STATUS_CATEGORY.GROUP_STATUS:t?o("WAWebWamEnumStatusCategory").STATUS_CATEGORY.CHANNEL_STATUS:o("WAWebWamEnumStatusCategory").STATUS_CATEGORY.REGULAR_STATUS}function S(t){var r=t.attributionType,a=t.isGroupStatus,i=t.isNewsletterStatus,l=t.msg,s=t.viewerSessionId,u={attributionType:b(r),statusCategory:v(a,i),viewerActionType:o("WAWebWamEnumStatusViewActionType").STATUS_VIEW_ACTION_TYPE.ATTRIBUTION_TAPPED};if(!o("WAWebStatusGatingUtils").isStatusViewerActionPsaLinkClickLoggingEnabled()){new(o("WAWebStatusViewerActionWamEvent")).StatusViewerActionWamEvent(u).commit();return}n("asyncToGeneratorRuntime").asyncToGenerator(function*(){var t;try{t=yield o("WAWebStatusLoggingUtils").statusIdForLogging(l)}catch(t){o("WALogger").ERROR(e||(e=babelHelpers.taggedTemplateLiteralLoose(["logAttributionTapped: status id hashing failed ",""])),t).sendLogs("status-viewer-action-status-id")}new(o("WAWebStatusViewerActionWamEvent")).StatusViewerActionWamEvent(babelHelpers.extends({},u,{statusId:t,statusViewerSessionId:s!=null?s:void 0})).commit()})()}function R(e,t){var n=o("react-compiler-runtime").c(4),r=g(0),a=r[0],i=r[1],l,s;return n[0]!==e||n[1]!==t?(l=function(){if(!(e<=1||!t)){var n=window.setInterval(function(){i(L)},y);return(function(){window.clearInterval(n)})}},s=[e,t],n[0]=e,n[1]=t,n[2]=l,n[3]=s):(l=n[2],s=n[3]),_(l,s),e===0?0:a%e}function L(e){return e+1}function E(e){var t=o("react-compiler-runtime").c(10),n=e.durationInSeconds,r=e.playing,a=g(n),i=a[0],l=a[1],s=f(null),u=f(0),c=f(n),d,p;t[0]!==n?(d=function(){c.current=n},p=[n],t[0]=n,t[1]=d,t[2]=p):(d=t[1],p=t[2]),_(d,p);var h,y;t[3]!==r?(h=function(){if(!r){var e=s.current;e!=null&&(u.current=u.current+(window.performance.now()-e)/1e3,s.current=null);return}s.current=window.performance.now();var t,n=function(){var e=s.current;if(e!=null){var r=u.current+(window.performance.now()-e)/1e3;l(Math.max(0,c.current-r))}t=window.requestAnimationFrame(n)};return t=window.requestAnimationFrame(n),(function(){window.cancelAnimationFrame(t)})},y=[r],t[3]=r,t[4]=h,t[5]=y):(h=t[4],y=t[5]),_(h,y);var C;t[6]!==i?(C=o("WAWebStatusExternalShareUtils").formatDuration(Math.round(i)),t[6]=i,t[7]=C):C=t[7];var b;return t[8]!==C?(b=m.jsx("span",{children:C}),t[8]=C,t[9]=b):b=t[9],b}function k(e){var t,n=o("react-compiler-runtime").c(74),a=e.msg,i=e.onClickProfile,l=e.onDialogDismiss,u=e.onDialogOpen,d=e.onMentionsClick,p=e.playing,_=e.status,f=e.statusMediaController,g=e.viewerSessionId,y=e.xstyle,b=_.contact,v;if(n[0]===Symbol.for("react.memo_cache_sentinel")){var L;v=[(L=o("WAWebMsgGetters")).getFirstMusicAnnotationEmbeddedContent,L.getStatusMentioned,L.getIsGroupStatus,L.getIsNewsletterStatus,L.getIsSentByMe,L.getHasPaidPartnershipLabel,L.getHasReshareAttribution,L.getIsNewsletterStatusReshare],n[0]=v}else v=n[0];var k=o("useWAWebMsgValues").useMsgValues(a.id,v),I=k[0],T=k[1],D=k[2],x=k[3],$=k[4],P=k[5],N=k[6],M=k[7],w=o("WAWebContactCollection").ContactCollection.get(a.author),A;n[1]!==a?(A=a.safe(),n[1]=a,n[2]=A):A=n[2];var F=A,O=o("WAWebMessageAIContentLabel.react").useShouldShowAiContentLabel(F),B=o("WAWebStatusExternalShareUtils").getExternalShareInfo(a),W=[];if(x&&o("WAWebNewsletterGatingUtils").isNewsletterStatusReceiverEnabled()){var q;n[3]===Symbol.for("react.memo_cache_sentinel")?(q=s._(/*BTDS*/"Channel status"),n[3]=q):q=n[3];var U;n[4]===Symbol.for("react.memo_cache_sentinel")?(U={type:"newsletterStatus",content:q,onClick:null,icon:m.jsx(r("WDSIconWdsIcChannels.react"),{width:14,height:14})},n[4]=U):U=n[4],W.push(U)}if(O){var V;if(n[5]!==a.id.remote){var H;V=(H=r("WAWebNewsletterMetadataCollection").get(a.id.remote))==null?void 0:H.iAmAdminOrOwner(),n[5]=a.id.remote,n[6]=V}else V=n[6];var G=V===!0,z;n[7]===Symbol.for("react.memo_cache_sentinel")?(z=s._(/*BTDS*/"AI content"),n[7]=z):z=n[7];var j;n[8]!==G||n[9]!==l||n[10]!==u?(j=function(){u==null||u(),o("WAWebNewsletterAiContentInfoModalOpener").openAiContentInfoModal(G?o("WAWebNewsletterAiContentInfoModalTypes").AiContentModalVariant.ADMIN_INFO:o("WAWebNewsletterAiContentInfoModalTypes").AiContentModalVariant.VIEWER_INFO,{onClose:function(){return l==null?void 0:l()}})},n[8]=G,n[9]=l,n[10]=u,n[11]=j):j=n[11];var K;n[12]===Symbol.for("react.memo_cache_sentinel")?(K=m.jsx(r("WDSIconWdsIcAiContent.react"),{width:14,height:14}),n[12]=K):K=n[12];var Q;n[13]!==j?(Q={type:"aiContent",content:z,onClick:j,icon:K},n[13]=j,n[14]=Q):Q=n[14],W.push(Q)}if(P===!0&&o("WAWebNewsletterGatingUtils").isChannelDSA26ReceiverEnabled()){var X;n[15]===Symbol.for("react.memo_cache_sentinel")?(X={type:"paidPartnership",content:s._(/*BTDS*/"Paid partnership"),onClick:null,icon:null},n[15]=X):X=n[15],W.push(X)}if(I!=null){var Y;n[16]!==x?(Y=x&&!o("WAWebMusicGatingUtils").isNewsletterStatusMusicConsumptionEnabled(),n[16]=x,n[17]=Y):Y=n[17];var J=Y,Z=!J,ee;n[18]!==a||n[19]!==I||n[20]!==p||n[21]!==Z?(ee=m.jsx(o("WAWebMusicStatusAttribution.react").MusicAttribution,{musicMetadata:I,msg:a,playing:p,tappable:Z}),n[18]=a,n[19]=I,n[20]=p,n[21]=Z,n[22]=ee):ee=n[22];var te;n[23]!==J||n[24]!==a||n[25]!==I||n[26]!==f?(te=J?null:function(){return o("WAWebMusicStatusAttribution.react").showMusicStatusAttributionModal({msg:a,musicMetadata:I,statusMediaController:f})},n[23]=J,n[24]=a,n[25]=I,n[26]=f,n[27]=te):te=n[27];var ne;n[28]!==ee||n[29]!==te?(ne={type:"music",content:ee,onClick:te,icon:null,showChevron:!1},n[28]=ee,n[29]=te,n[30]=ne):ne=n[30],W.push(ne)}if(T===!0&&o("WAWebStatusGatingUtils").isStatusPrivateMentionsReceiveEnabled()){var re;n[31]===Symbol.for("react.memo_cache_sentinel")?(re=s._(/*BTDS*/"Mentioned privately"),n[31]=re):re=n[31];var oe;n[32]===Symbol.for("react.memo_cache_sentinel")?(oe=m.jsx(o("WAWebMentionsRefreshedIcon.react").MentionsRefreshedIcon,{width:14,height:14}),n[32]=oe):oe=n[32];var ae;n[33]!==d?(ae={type:"mention",content:re,onClick:d,icon:oe},n[33]=d,n[34]=ae):ae=n[34],W.push(ae)}if(D&&w!=null){var ie=o("WAWebContactGetters").getIsMe(w),le=ie?"You":o("WAWebFrontendContactGetters").getFormattedName(w);W.push({type:"groupStatus",content:s._(/*BTDS*/"Added by {Author Name}",[s._param("Author Name",le)]),onClick:function(){i(w.id)},icon:null,showChevron:!ie})}var se=a.statusAudienceMetadata,ue=se!=null&&o("WAWebStatusGatingUtils").isStatusCloseFriendsViewerSideEnabled();if(ue){var ce,de;if(n[35]!==(se==null?void 0:se.listName)){var me;de=(me=se==null?void 0:se.listName)!=null?me:s._(/*BTDS*/"Close friends"),n[35]=se==null?void 0:se.listName,n[36]=de}else de=n[36];var pe=de,_e=(ce=se==null?void 0:se.listEmoji)!=null?ce:"\u2B50",fe;n[37]!==pe?(fe=s._(/*BTDS*/"{List Name}",[s._param("List Name",pe)]),n[37]=pe,n[38]=fe):fe=n[38];var ge;n[39]!==$||n[40]!==pe||n[41]!==_e||n[42]!==l||n[43]!==u||n[44]!==b?(ge=function(){u==null||u(),o("WAWebStatusCloseFriendsDialog.react").showCloseFriendsDialog({contactId:b==null?void 0:b.id,contactName:b!=null?o("WAWebFrontendContactGetters").getFormattedName(b):"",emoji:_e,isOwnStatus:$,listName:pe,onDismiss:function(){l==null||l()}})},n[39]=$,n[40]=pe,n[41]=_e,n[42]=l,n[43]=u,n[44]=b,n[45]=ge):ge=n[45];var he;n[46]!==fe||n[47]!==ge?(he={type:"closeFriends",content:fe,onClick:ge,icon:null},n[46]=fe,n[47]=ge,n[48]=he):he=n[48],W.push(he)}var ye=(t=a.forwardedNewsletterMessageInfo)==null?void 0:t.newsletterId,Ce=N&&(o("WAWebStatusGatingUtils").isStatusReshareAttributionEnabled()||M&&o("WAWebNewsletterGatingUtils").isNewsletterStatusReshareEnabled());if(Ce){var be;n[49]===Symbol.for("react.memo_cache_sentinel")?(be=s._(/*BTDS*/"Reshared"),n[49]=be):be=n[49];var ve;n[50]===Symbol.for("react.memo_cache_sentinel")?(ve={type:"reshare",content:be,onClick:null,icon:m.jsx(r("WDSIconIcRepeat.react"),{width:14,height:14})},n[50]=ve):ve=n[50],W.push(ve)}if(ye!=null&&(o("WAWebNewsletterGatingUtils").isNewsletterLinksOnStatusConsumptionEnabled()||M)){var Se;n[51]===Symbol.for("react.memo_cache_sentinel")?(Se=s._(/*BTDS*/"View channel"),n[51]=Se):Se=n[51];var Re;n[52]!==ye?(Re=function(){return o("WAWebStatusNavigateToNewsletterUtils").navigateToNewsletter(ye)},n[52]=ye,n[53]=Re):Re=n[53];var Le;n[54]===Symbol.for("react.memo_cache_sentinel")?(Le=m.jsx(r("WDSIconWdsIcChannels.react"),{width:14,height:14}),n[54]=Le):Le=n[54];var Ee;n[55]!==Re?(Ee={type:"viewChannel",content:Se,onClick:Re,icon:Le},n[55]=Re,n[56]=Ee):Ee=n[56],W.push(Ee)}if(B!=null){var ke=B.durationInSeconds;W.push({type:"externalShare",content:m.jsxs(o("WAWebFlex.react").FlexRow,{align:"center",justify:"start",children:[m.jsx(o("WAWebFlex.react").FlexItem,{align:"center",grow:0,shrink:0,children:B.displayText}),B.source===o("WAWebProtobufsStatusAttributions.pb").StatusAttribution$ExternalShare$Source.INSTAGRAM&&ke!=null&&ke!==0&&m.jsxs(o("WAWebFlex.react").FlexRow,{children:[m.jsx(o("WAWebFlex.react").FlexItem,{className:"x1wbi8v6 x7g7pl8",children:"\u2022"}),m.jsx(E,{durationInSeconds:ke,playing:p})]})]}),onClick:function(){o("WAWebExternalLink.react").openExternalLink(B.url)},icon:o("WAWebStatusExternalShareUtils").getExternalShareIcon(B.source)})}var Ie=W.slice(0,C),Te=R(Ie.length,p);if(b!=null&&o("WAWebContactGetters").getIsPSA(b))return null;var De=Ie.length>0?Ie[Te]:null,xe=o("WAWebFlex.react").FlexRow,$e="center",Pe="start",Ne=(c||(c=r("stylex")))([ue&&h.subtitleRow,y]),Me;n[57]===Symbol.for("react.memo_cache_sentinel")?(Me={className:"x78zum5 x6s0dn4"},n[57]=Me):Me=n[57];var we;n[58]!==a.t?(we=o("WAWebClock").Clock.relativeDateAndTimeStr(a.t),n[58]=a.t,n[59]=we):we=n[59];var Ae;n[60]!==we?(Ae=m.jsx(o("WAWebFlex.react").FlexItem,{align:"center",grow:0,shrink:0,children:m.jsx("div",babelHelpers.extends({},Me,{children:m.jsx(r("WDSText.react"),{type:"Body2",colorName:"persistentAlwaysWhite",maxLines:1,children:we})}))}),n[60]=we,n[61]=Ae):Ae=n[61];var Fe;if(n[62]!==De||n[63]!==ue||n[64]!==D||n[65]!==x||n[66]!==a||n[67]!==g){var Oe;Fe=De!=null&&m.jsx("span",{className:"x17t9dm2 xeuugli",children:m.jsx(r("WAWebStatusSubtitleAttribution.react"),{isCloseFriends:ue,onClick:De.onClick!=null?function(e){o("WAWebStopEvent").stopPropagation(e),S({attributionType:De.type,isGroupStatus:D,isNewsletterStatus:x,msg:a,viewerSessionId:g}),De.onClick==null||De.onClick(e)}:null,icon:De.icon,enableChevronIcon:(Oe=De.showChevron)!=null?Oe:De.onClick!=null,children:De.content})}),n[62]=De,n[63]=ue,n[64]=D,n[65]=x,n[66]=a,n[67]=g,n[68]=Fe}else Fe=n[68];var Be;return n[69]!==xe||n[70]!==Ne||n[71]!==Ae||n[72]!==Fe?(Be=m.jsxs(xe,{align:$e,justify:Pe,className:Ne,children:[Ae,Fe]}),n[69]=xe,n[70]=Ne,n[71]=Ae,n[72]=Fe,n[73]=Be):Be=n[73],Be}l.default=k}),226); diff --git a/crates/wa-wam/tests/fixtures/status-subtitle-old.js b/crates/wa-wam/tests/fixtures/status-subtitle-old.js new file mode 100644 index 00000000..dfe99ffb --- /dev/null +++ b/crates/wa-wam/tests/fixtures/status-subtitle-old.js @@ -0,0 +1,4 @@ +// {"waVersion": "2.3000.1045368834", "bundleSha256": "72622fd528bcdb8719660018f754e37ce201276a4c5fdead04f82bc81648de8d", "start": 505398, "end": 506430} +__d("WAWebStatusViewerActionWamEvent",["WAWebWamCodegenUtils","WAWebWamEnumInlineVideoType","WAWebWamEnumStatusCategory","WAWebWamEnumStatusViewActionType","WAWebWamEnumTopBarAttributionType","WAWebWamEnumUrlStatusClicked","WAWebWamEnumUrlStatusType"],(function(t,n,r,o,a,i,l){var e,s=(e=o("WAWebWamCodegenUtils")).defineEvents({StatusViewerAction:[6692,{attributionType:[1,o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE],attributionTypes:[3,e.TYPES.STRING],externalSourceDomainType:[5,o("WAWebWamEnumInlineVideoType").INLINE_VIDEO_TYPE],psaCampaignId:[11,e.TYPES.STRING],statusCategory:[4,o("WAWebWamEnumStatusCategory").STATUS_CATEGORY],statusId:[12,e.TYPES.STRING],statusViewerSessionId:[13,e.TYPES.INTEGER],urlStatusClicked:[6,o("WAWebWamEnumUrlStatusClicked").URL_STATUS_CLICKED],urlStatusType:[7,o("WAWebWamEnumUrlStatusType").URL_STATUS_TYPE],viewerActionType:[2,o("WAWebWamEnumStatusViewActionType").STATUS_VIEW_ACTION_TYPE]},[1,1,1],"regular"]},{StatusViewerAction:[]});l.StatusViewerActionWamEvent=s}),98); +// {"waVersion": "2.3000.1045368834", "bundleSha256": "72622fd528bcdb8719660018f754e37ce201276a4c5fdead04f82bc81648de8d", "start": 506432, "end": 518491} +__d("WAWebStatusSubtitle.react",["fbt","invariant","WAWebClock","WAWebContactCollection","WAWebContactGetters","WAWebExternalLink.react","WAWebFlex.react","WAWebFrontendContactGetters","WAWebMentionsRefreshedIcon.react","WAWebMessageAIContentLabel.react","WAWebMsgGetters","WAWebMusicGatingUtils","WAWebMusicStatusAttribution.react","WAWebNewsletterAiContentInfoModalOpener","WAWebNewsletterAiContentInfoModalTypes","WAWebNewsletterGatingUtils","WAWebNewsletterMetadataCollection","WAWebProtobufsStatusAttributions.pb","WAWebStatusCloseFriendsDialog.react","WAWebStatusExternalShareUtils","WAWebStatusGatingUtils","WAWebStatusNavigateToNewsletterUtils","WAWebStatusSubtitleAttribution.react","WAWebStatusViewerActionWamEvent","WAWebStopEvent","WAWebWamEnumStatusCategory","WAWebWamEnumStatusViewActionType","WAWebWamEnumTopBarAttributionType","WDSIconIcRepeat.react","WDSIconWdsIcAiContent.react","WDSIconWdsIcChannels.react","WDSText.react","react","react-compiler-runtime","stylex","useWAWebMsgValues"],(function(t,n,r,o,a,i,l,s,u){var e,c,d=c||(c=o("react")),m=c,p=m.useEffect,_=m.useRef,f=m.useState,g={subtitleRow:{overflowX:"x6ikm8r",overflowY:"x10wlt62",minWidth:"xeuugli",$$css:!0}},h=3e3,y=3;function C(e){return e==="music"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.MUSIC:e==="mention"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.MENTION:e==="groupStatus"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.GROUP_STATUS:e==="closeFriends"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.CLOSE_SHARING:e==="reshare"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.RESHARED_STATUS:e==="externalShare"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.CROSSPOSTING:e==="newsletterStatus"||e==="viewChannel"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.CHANNEL_STATUS:e==="aiContent"?o("WAWebWamEnumTopBarAttributionType").TOP_BAR_ATTRIBUTION_TYPE.AI_CREATED:e==="paidPartnership"?u(!1,"paidPartnership attribution is not tappable"):(function(){throw Error("Match: No case succesfully matched. Make exhaustive or add a wildcard case using '_'. Argument: "+e)})()}function b(e,t){return e?o("WAWebWamEnumStatusCategory").STATUS_CATEGORY.GROUP_STATUS:t?o("WAWebWamEnumStatusCategory").STATUS_CATEGORY.CHANNEL_STATUS:o("WAWebWamEnumStatusCategory").STATUS_CATEGORY.REGULAR_STATUS}function v(e){var t=e.attributionType,n=e.isGroupStatus,r=e.isNewsletterStatus;new(o("WAWebStatusViewerActionWamEvent")).StatusViewerActionWamEvent({viewerActionType:o("WAWebWamEnumStatusViewActionType").STATUS_VIEW_ACTION_TYPE.ATTRIBUTION_TAPPED,attributionType:C(t),statusCategory:b(n,r)}).commit()}function S(e,t){var n=o("react-compiler-runtime").c(4),r=f(0),a=r[0],i=r[1],l,s;return n[0]!==e||n[1]!==t?(l=function(){if(!(e<=1||!t)){var n=window.setInterval(function(){i(R)},h);return(function(){window.clearInterval(n)})}},s=[e,t],n[0]=e,n[1]=t,n[2]=l,n[3]=s):(l=n[2],s=n[3]),p(l,s),e===0?0:a%e}function R(e){return e+1}function L(e){var t=o("react-compiler-runtime").c(10),n=e.durationInSeconds,r=e.playing,a=f(n),i=a[0],l=a[1],s=_(null),u=_(0),c=_(n),m,g;t[0]!==n?(m=function(){c.current=n},g=[n],t[0]=n,t[1]=m,t[2]=g):(m=t[1],g=t[2]),p(m,g);var h,y;t[3]!==r?(h=function(){if(!r){var e=s.current;e!=null&&(u.current=u.current+(window.performance.now()-e)/1e3,s.current=null);return}s.current=window.performance.now();var t,n=function(){var e=s.current;if(e!=null){var r=u.current+(window.performance.now()-e)/1e3;l(Math.max(0,c.current-r))}t=window.requestAnimationFrame(n)};return t=window.requestAnimationFrame(n),(function(){window.cancelAnimationFrame(t)})},y=[r],t[3]=r,t[4]=h,t[5]=y):(h=t[4],y=t[5]),p(h,y);var C;t[6]!==i?(C=o("WAWebStatusExternalShareUtils").formatDuration(Math.round(i)),t[6]=i,t[7]=C):C=t[7];var b;return t[8]!==C?(b=d.jsx("span",{children:C}),t[8]=C,t[9]=b):b=t[9],b}function E(t){var n,a=o("react-compiler-runtime").c(72),i=t.msg,l=t.onClickProfile,u=t.onDialogDismiss,c=t.onDialogOpen,m=t.onMentionsClick,p=t.playing,_=t.status,f=t.statusMediaController,h=t.xstyle,C=_.contact,b;if(a[0]===Symbol.for("react.memo_cache_sentinel")){var R;b=[(R=o("WAWebMsgGetters")).getFirstMusicAnnotationEmbeddedContent,R.getStatusMentioned,R.getIsGroupStatus,R.getIsNewsletterStatus,R.getIsSentByMe,R.getHasPaidPartnershipLabel,R.getHasReshareAttribution,R.getIsNewsletterStatusReshare],a[0]=b}else b=a[0];var E=o("useWAWebMsgValues").useMsgValues(i.id,b),k=E[0],I=E[1],T=E[2],D=E[3],x=E[4],$=E[5],P=E[6],N=E[7],M=o("WAWebContactCollection").ContactCollection.get(i.author),w;a[1]!==i?(w=i.safe(),a[1]=i,a[2]=w):w=a[2];var A=w,F=o("WAWebMessageAIContentLabel.react").useShouldShowAiContentLabel(A),O=o("WAWebStatusExternalShareUtils").getExternalShareInfo(i),B=[];if(D&&o("WAWebNewsletterGatingUtils").isNewsletterStatusReceiverEnabled()){var W;a[3]===Symbol.for("react.memo_cache_sentinel")?(W=s._(/*BTDS*/"Channel status"),a[3]=W):W=a[3];var q;a[4]===Symbol.for("react.memo_cache_sentinel")?(q={type:"newsletterStatus",content:W,onClick:null,icon:d.jsx(r("WDSIconWdsIcChannels.react"),{width:14,height:14})},a[4]=q):q=a[4],B.push(q)}if(F){var U;if(a[5]!==i.id.remote){var V;U=(V=r("WAWebNewsletterMetadataCollection").get(i.id.remote))==null?void 0:V.iAmAdminOrOwner(),a[5]=i.id.remote,a[6]=U}else U=a[6];var H=U===!0,G;a[7]===Symbol.for("react.memo_cache_sentinel")?(G=s._(/*BTDS*/"AI content"),a[7]=G):G=a[7];var z;a[8]!==H||a[9]!==u||a[10]!==c?(z=function(){c==null||c(),o("WAWebNewsletterAiContentInfoModalOpener").openAiContentInfoModal(H?o("WAWebNewsletterAiContentInfoModalTypes").AiContentModalVariant.ADMIN_INFO:o("WAWebNewsletterAiContentInfoModalTypes").AiContentModalVariant.VIEWER_INFO,{onClose:function(){return u==null?void 0:u()}})},a[8]=H,a[9]=u,a[10]=c,a[11]=z):z=a[11];var j;a[12]===Symbol.for("react.memo_cache_sentinel")?(j=d.jsx(r("WDSIconWdsIcAiContent.react"),{width:14,height:14}),a[12]=j):j=a[12];var K;a[13]!==z?(K={type:"aiContent",content:G,onClick:z,icon:j},a[13]=z,a[14]=K):K=a[14],B.push(K)}if($===!0&&o("WAWebNewsletterGatingUtils").isChannelDSA26ReceiverEnabled()){var Q;a[15]===Symbol.for("react.memo_cache_sentinel")?(Q={type:"paidPartnership",content:s._(/*BTDS*/"Paid partnership"),onClick:null,icon:null},a[15]=Q):Q=a[15],B.push(Q)}if(k!=null){var X;a[16]!==D?(X=D&&!o("WAWebMusicGatingUtils").isNewsletterStatusMusicConsumptionEnabled(),a[16]=D,a[17]=X):X=a[17];var Y=X,J=!Y,Z;a[18]!==i||a[19]!==k||a[20]!==p||a[21]!==J?(Z=d.jsx(o("WAWebMusicStatusAttribution.react").MusicAttribution,{musicMetadata:k,msg:i,playing:p,tappable:J}),a[18]=i,a[19]=k,a[20]=p,a[21]=J,a[22]=Z):Z=a[22];var ee;a[23]!==Y||a[24]!==i||a[25]!==k||a[26]!==f?(ee=Y?null:function(){return o("WAWebMusicStatusAttribution.react").showMusicStatusAttributionModal({msg:i,musicMetadata:k,statusMediaController:f})},a[23]=Y,a[24]=i,a[25]=k,a[26]=f,a[27]=ee):ee=a[27];var te;a[28]!==Z||a[29]!==ee?(te={type:"music",content:Z,onClick:ee,icon:null,showChevron:!1},a[28]=Z,a[29]=ee,a[30]=te):te=a[30],B.push(te)}if(I===!0&&o("WAWebStatusGatingUtils").isStatusPrivateMentionsReceiveEnabled()){var ne;a[31]===Symbol.for("react.memo_cache_sentinel")?(ne=s._(/*BTDS*/"Mentioned privately"),a[31]=ne):ne=a[31];var re;a[32]===Symbol.for("react.memo_cache_sentinel")?(re=d.jsx(o("WAWebMentionsRefreshedIcon.react").MentionsRefreshedIcon,{width:14,height:14}),a[32]=re):re=a[32];var oe;a[33]!==m?(oe={type:"mention",content:ne,onClick:m,icon:re},a[33]=m,a[34]=oe):oe=a[34],B.push(oe)}if(T&&M!=null){var ae=o("WAWebContactGetters").getIsMe(M),ie=ae?"You":o("WAWebFrontendContactGetters").getFormattedName(M);B.push({type:"groupStatus",content:s._(/*BTDS*/"Added by {Author Name}",[s._param("Author Name",ie)]),onClick:function(){l(M.id)},icon:null,showChevron:!ae})}var le=i.statusAudienceMetadata,se=le!=null&&o("WAWebStatusGatingUtils").isStatusCloseFriendsViewerSideEnabled();if(se){var ue,ce;if(a[35]!==(le==null?void 0:le.listName)){var de;ce=(de=le==null?void 0:le.listName)!=null?de:s._(/*BTDS*/"Close friends"),a[35]=le==null?void 0:le.listName,a[36]=ce}else ce=a[36];var me=ce,pe=(ue=le==null?void 0:le.listEmoji)!=null?ue:"\u2B50",_e;a[37]!==me?(_e=s._(/*BTDS*/"{List Name}",[s._param("List Name",me)]),a[37]=me,a[38]=_e):_e=a[38];var fe;a[39]!==x||a[40]!==me||a[41]!==pe||a[42]!==u||a[43]!==c||a[44]!==C?(fe=function(){c==null||c(),o("WAWebStatusCloseFriendsDialog.react").showCloseFriendsDialog({contactId:C==null?void 0:C.id,contactName:C!=null?o("WAWebFrontendContactGetters").getFormattedName(C):"",emoji:pe,isOwnStatus:x,listName:me,onDismiss:function(){u==null||u()}})},a[39]=x,a[40]=me,a[41]=pe,a[42]=u,a[43]=c,a[44]=C,a[45]=fe):fe=a[45];var ge;a[46]!==_e||a[47]!==fe?(ge={type:"closeFriends",content:_e,onClick:fe,icon:null},a[46]=_e,a[47]=fe,a[48]=ge):ge=a[48],B.push(ge)}var he=(n=i.forwardedNewsletterMessageInfo)==null?void 0:n.newsletterId,ye=P&&(o("WAWebStatusGatingUtils").isStatusReshareAttributionEnabled()||N&&o("WAWebNewsletterGatingUtils").isNewsletterStatusReshareEnabled());if(ye){var Ce;a[49]===Symbol.for("react.memo_cache_sentinel")?(Ce=s._(/*BTDS*/"Reshared"),a[49]=Ce):Ce=a[49];var be;a[50]===Symbol.for("react.memo_cache_sentinel")?(be={type:"reshare",content:Ce,onClick:null,icon:d.jsx(r("WDSIconIcRepeat.react"),{width:14,height:14})},a[50]=be):be=a[50],B.push(be)}if(he!=null&&(o("WAWebNewsletterGatingUtils").isNewsletterLinksOnStatusConsumptionEnabled()||N)){var ve;a[51]===Symbol.for("react.memo_cache_sentinel")?(ve=s._(/*BTDS*/"View channel"),a[51]=ve):ve=a[51];var Se;a[52]!==he?(Se=function(){return o("WAWebStatusNavigateToNewsletterUtils").navigateToNewsletter(he)},a[52]=he,a[53]=Se):Se=a[53];var Re;a[54]===Symbol.for("react.memo_cache_sentinel")?(Re=d.jsx(r("WDSIconWdsIcChannels.react"),{width:14,height:14}),a[54]=Re):Re=a[54];var Le;a[55]!==Se?(Le={type:"viewChannel",content:ve,onClick:Se,icon:Re},a[55]=Se,a[56]=Le):Le=a[56],B.push(Le)}if(O!=null){var Ee=O.durationInSeconds;B.push({type:"externalShare",content:d.jsxs(o("WAWebFlex.react").FlexRow,{align:"center",justify:"start",children:[d.jsx(o("WAWebFlex.react").FlexItem,{align:"center",grow:0,shrink:0,children:O.displayText}),O.source===o("WAWebProtobufsStatusAttributions.pb").StatusAttribution$ExternalShare$Source.INSTAGRAM&&Ee!=null&&Ee!==0&&d.jsxs(o("WAWebFlex.react").FlexRow,{children:[d.jsx(o("WAWebFlex.react").FlexItem,{className:"x1wbi8v6 x7g7pl8",children:"\u2022"}),d.jsx(L,{durationInSeconds:Ee,playing:p})]})]}),onClick:function(){o("WAWebExternalLink.react").openExternalLink(O.url)},icon:o("WAWebStatusExternalShareUtils").getExternalShareIcon(O.source)})}var ke=B.slice(0,y),Ie=S(ke.length,p);if(C!=null&&o("WAWebContactGetters").getIsPSA(C))return null;var Te=ke.length>0?ke[Ie]:null,De=o("WAWebFlex.react").FlexRow,xe="center",$e="start",Pe=(e||(e=r("stylex")))([se&&g.subtitleRow,h]),Ne;a[57]===Symbol.for("react.memo_cache_sentinel")?(Ne={className:"x78zum5 x6s0dn4"},a[57]=Ne):Ne=a[57];var Me;a[58]!==i.t?(Me=o("WAWebClock").Clock.relativeDateAndTimeStr(i.t),a[58]=i.t,a[59]=Me):Me=a[59];var we;a[60]!==Me?(we=d.jsx(o("WAWebFlex.react").FlexItem,{align:"center",grow:0,shrink:0,children:d.jsx("div",babelHelpers.extends({},Ne,{children:d.jsx(r("WDSText.react"),{type:"Body2",colorName:"persistentAlwaysWhite",maxLines:1,children:Me})}))}),a[60]=Me,a[61]=we):we=a[61];var Ae;if(a[62]!==Te||a[63]!==se||a[64]!==T||a[65]!==D){var Fe;Ae=Te!=null&&d.jsx("span",{className:"x17t9dm2 xeuugli",children:d.jsx(r("WAWebStatusSubtitleAttribution.react"),{isCloseFriends:se,onClick:Te.onClick!=null?function(e){o("WAWebStopEvent").stopPropagation(e),v({attributionType:Te.type,isGroupStatus:T,isNewsletterStatus:D}),Te.onClick==null||Te.onClick(e)}:null,icon:Te.icon,enableChevronIcon:(Fe=Te.showChevron)!=null?Fe:Te.onClick!=null,children:Te.content})}),a[62]=Te,a[63]=se,a[64]=T,a[65]=D,a[66]=Ae}else Ae=a[66];var Oe;return a[67]!==De||a[68]!==Pe||a[69]!==we||a[70]!==Ae?(Oe=d.jsxs(De,{align:xe,justify:$e,className:Pe,children:[we,Ae]}),a[67]=De,a[68]=Pe,a[69]=we,a[70]=Ae,a[71]=Oe):Oe=a[71],Oe}l.default=E}),226); diff --git a/generated/manifest.json b/generated/manifest.json index 47c0f216..67df1b24 100644 --- a/generated/manifest.json +++ b/generated/manifest.json @@ -88,21 +88,21 @@ "types": 27 }, "wam": { - "callSiteFieldValues": 790, - "callSiteFields": 3289, - "callSites": 810, + "callSiteFieldValues": 796, + "callSiteFields": 3446, + "callSites": 812, "constants": 6, "constructions": 874, "dropsByReason": { "construction of an event with no catalog entry": 29, "unreadConstructionArgument.call": 1, - "unreadConstructionArgument.identifier": 89, + "unreadConstructionArgument.identifier": 75, "unreadConstructionArgument.member": 3 }, - "duplicateCallSites": 35, + "duplicateCallSites": 33, "events": 439, "globals": 46, - "partialCallSites": 126, + "partialCallSites": 114, "privateStatsIds": 9, "samplingWeightOverrides": 4 } @@ -165,7 +165,7 @@ "wam": { "file": "wam/index.json", "schema": "schema/wam.schema.json", - "sha256": "8fb471e36f1054dff25bf78acbe1bcf4163fab22909a0c756753a9400b093047" + "sha256": "d92a4cc88baeeaa9a2a9d8695371d01969b8025de589a605f8f33569a87abbbd" }, "wasm": { "file": "wasm/index.json", diff --git a/generated/wam/index.json b/generated/wam/index.json index eb166271..ff4b34d1 100644 --- a/generated/wam/index.json +++ b/generated/wam/index.json @@ -23981,6 +23981,42 @@ } } ] + }, + { + "module": "WAWebFilterLogging", + "fields": [ + { + "name": "actionType", + "write": "constructor", + "value": { + "kind": "enumMember", + "module": "WAWebWamEnumChatFilterActionTypes", + "key": "FILTER_VIEWED" + } + }, + { + "name": "filterType", + "write": "constructor", + "value": { + "kind": "enumMember", + "module": "WAWebWamEnumChatFilterTypes", + "key": "LABEL" + } + }, + { + "name": "sessionId", + "write": "constructor" + }, + { + "name": "targetScreen", + "write": "constructor", + "value": { + "kind": "enumMember", + "module": "WAWebWamEnumChatFilterTargetScreen", + "key": "CHAT_LIST" + } + } + ] } ] }, @@ -26107,7 +26143,116 @@ "callSites": [ { "module": "WAWebWamFingerprintReporter", - "partial": true + "fields": [ + { + "name": "automationSignals", + "write": "constructor" + }, + { + "name": "browserEngine", + "write": "constructor" + }, + { + "name": "chromeStructure", + "write": "constructor" + }, + { + "name": "connectionRtt", + "write": "constructor" + }, + { + "name": "cpuMake", + "write": "constructor" + }, + { + "name": "deviceMemory", + "write": "constructor" + }, + { + "name": "extentionIds", + "write": "constructor" + }, + { + "name": "externalSources", + "write": "constructor" + }, + { + "name": "foreignDbList", + "write": "constructor" + }, + { + "name": "gpuMake", + "write": "constructor" + }, + { + "name": "hasChrome", + "write": "constructor" + }, + { + "name": "hasTaskbar", + "write": "constructor" + }, + { + "name": "hasWebShare", + "write": "constructor" + }, + { + "name": "historyLength", + "write": "constructor" + }, + { + "name": "mimeTypeCount", + "write": "constructor" + }, + { + "name": "notificationPermission", + "write": "constructor" + }, + { + "name": "pdfViewerEnabled", + "write": "constructor" + }, + { + "name": "platformEstimate", + "write": "constructor" + }, + { + "name": "pluginCount", + "write": "constructor" + }, + { + "name": "screenResolution", + "write": "constructor" + }, + { + "name": "sessionStorageLength", + "write": "constructor" + }, + { + "name": "timezone", + "write": "constructor" + }, + { + "name": "touchPresence", + "write": "constructor" + }, + { + "name": "viewportSize", + "write": "constructor" + }, + { + "name": "webcCanvasFingerprint", + "write": "constructor" + }, + { + "name": "webcWebglFingerprint", + "write": "constructor" + }, + { + "name": "webcWindowNavigatorWebdriver", + "write": "constructor" + } + ] } ] }, @@ -27498,12 +27643,39 @@ { "module": "WAWebSendStickerAction", "fields": [ + { + "name": "stickerIsAi", + "write": "constructor" + }, + { + "name": "stickerIsAnimated", + "write": "constructor" + }, + { + "name": "stickerIsFirstParty", + "write": "constructor" + }, + { + "name": "stickerIsFromStickerMaker", + "write": "constructor" + }, + { + "name": "stickerIsLottie", + "write": "constructor" + }, + { + "name": "stickerIsPremium", + "write": "constructor" + }, { "name": "stickerMakerSourceType", "write": "assigned" + }, + { + "name": "stickerSendOrigin", + "write": "constructor" } - ], - "partial": true + ] } ] }, @@ -29456,7 +29628,66 @@ }, { "module": "WAWebPaymentsUserActionLogging", - "partial": true + "fields": [ + { + "name": "actionTarget", + "write": "constructor" + }, + { + "name": "paymentActionType", + "write": "constructor" + }, + { + "name": "paymentsCountryCode", + "write": "constructor" + }, + { + "name": "paymentsEventId", + "write": "constructor" + }, + { + "name": "queryParams", + "write": "constructor" + }, + { + "name": "referral", + "write": "constructor" + }, + { + "name": "screen", + "write": "constructor" + } + ] + }, + { + "module": "WAWebPaymentsUserActionLogging", + "fields": [ + { + "name": "actionTarget", + "write": "constructor" + }, + { + "name": "paymentActionType", + "write": "constructor", + "value": { + "kind": "enumMember", + "module": "WAWebWamEnumPaymentActionTypes", + "key": "CLICK" + } + }, + { + "name": "paymentsCountryCode", + "write": "constructor" + }, + { + "name": "queryParams", + "write": "constructor" + }, + { + "name": "screen", + "write": "constructor" + } + ] }, { "module": "WAWebPixWamLogger", @@ -48105,7 +48336,44 @@ "callSites": [ { "module": "WAWebMexLogging", - "partial": true + "fields": [ + { + "name": "mexEventV2DurationMs", + "write": "constructor" + }, + { + "name": "mexEventV2EndTime", + "write": "constructor" + }, + { + "name": "mexEventV2ErrorCodes", + "write": "constructor" + }, + { + "name": "mexEventV2Errors", + "write": "constructor" + }, + { + "name": "mexEventV2HasData", + "write": "constructor" + }, + { + "name": "mexEventV2IsMex", + "write": "constructor" + }, + { + "name": "mexEventV2OperationName", + "write": "constructor" + }, + { + "name": "mexEventV2QueryId", + "write": "constructor" + }, + { + "name": "mexEventV2StartTime", + "write": "constructor" + } + ] } ] }, @@ -59430,7 +59698,28 @@ "callSites": [ { "module": "WAWebThreadLoggingVoip", - "partial": true + "fields": [ + { + "name": "callOffersReceived", + "write": "constructor" + }, + { + "name": "callOffersSent", + "write": "constructor" + }, + { + "name": "threadDs", + "write": "constructor" + }, + { + "name": "threadId", + "write": "constructor" + }, + { + "name": "totalCallDuration", + "write": "constructor" + } + ] } ] }, @@ -60137,7 +60426,24 @@ "callSites": [ { "module": "WAWebThreadLoggingAi", - "partial": true + "fields": [ + { + "name": "threadDs", + "write": "constructor" + }, + { + "name": "threadId", + "write": "constructor" + }, + { + "name": "totalMessageFromAgentCnt", + "write": "constructor" + }, + { + "name": "totalMessageToAgentCnt", + "write": "constructor" + } + ] } ] }, @@ -60286,7 +60592,20 @@ "callSites": [ { "module": "WAWebThreadLoggingNotification", - "partial": true + "fields": [ + { + "name": "isAGroup", + "write": "constructor" + }, + { + "name": "threadDs", + "write": "constructor" + }, + { + "name": "threadId", + "write": "constructor" + } + ] } ] }, @@ -60704,7 +61023,56 @@ "callSites": [ { "module": "WAWebThreadLoggingBiz", - "partial": true + "fields": [ + { + "name": "bizCatalogType", + "write": "constructor" + }, + { + "name": "chatOrigins", + "write": "constructor" + }, + { + "name": "commerceMsgsReceived", + "write": "constructor" + }, + { + "name": "commerceMsgsSent", + "write": "constructor" + }, + { + "name": "isCommerceViewed", + "write": "constructor" + }, + { + "name": "isCtaOnPdpClicked", + "write": "constructor" + }, + { + "name": "isUserAgent", + "write": "constructor" + }, + { + "name": "ordersSent", + "write": "constructor" + }, + { + "name": "pdpInquiriesSent", + "write": "constructor" + }, + { + "name": "pdpViews", + "write": "constructor" + }, + { + "name": "threadDs", + "write": "constructor" + }, + { + "name": "threadId", + "write": "constructor" + } + ] } ] }, @@ -61466,7 +61834,244 @@ "callSites": [ { "module": "WAWebThreadLoggingCoreConsumer", - "partial": true + "fields": [ + { + "name": "afterReadDuration", + "write": "constructor" + }, + { + "name": "afterReadMessagesExpired", + "write": "constructor" + }, + { + "name": "afterReadMessagesReceived", + "write": "constructor" + }, + { + "name": "afterReadMessagesSent", + "write": "constructor" + }, + { + "name": "afterReadMessagesUnreadExpired", + "write": "constructor" + }, + { + "name": "afterReadTurnedOff", + "write": "constructor" + }, + { + "name": "afterReadTurnedOn", + "write": "constructor" + }, + { + "name": "chatMuted", + "write": "constructor" + }, + { + "name": "chatOverflowClicks", + "write": "constructor" + }, + { + "name": "commentsReceived", + "write": "constructor" + }, + { + "name": "editedMsgsSent", + "write": "constructor" + }, + { + "name": "eventCreationMessagesReceived", + "write": "constructor" + }, + { + "name": "eventCreationMessagesSent", + "write": "constructor" + }, + { + "name": "eventResponseMessagesReceived", + "write": "constructor" + }, + { + "name": "eventResponseMessagesSent", + "write": "constructor" + }, + { + "name": "forwardMessagesReceived", + "write": "constructor" + }, + { + "name": "forwardMessagesSent", + "write": "constructor" + }, + { + "name": "groupMembershipReplies", + "write": "constructor" + }, + { + "name": "groupPrivateReplies", + "write": "constructor" + }, + { + "name": "groupSize", + "write": "constructor" + }, + { + "name": "groupStatusLikesOthersToOthers", + "write": "constructor" + }, + { + "name": "groupStatusLikesOthersToOwn", + "write": "constructor" + }, + { + "name": "groupStatusRepliesOthersToOthers", + "write": "constructor" + }, + { + "name": "groupStatusRepliesOthersToOwn", + "write": "constructor" + }, + { + "name": "groupStatusRepliesOwnToOthers", + "write": "constructor" + }, + { + "name": "groupStatusRepliesOwnToOwn", + "write": "constructor" + }, + { + "name": "hasReplied1On1", + "write": "constructor" + }, + { + "name": "hasUsername", + "write": "constructor" + }, + { + "name": "hasUsernamePin", + "write": "constructor" + }, + { + "name": "isAContact", + "write": "constructor" + }, + { + "name": "isAGroup", + "write": "constructor" + }, + { + "name": "isArchived", + "write": "constructor" + }, + { + "name": "isMessageYourself", + "write": "constructor" + }, + { + "name": "isPinned", + "write": "constructor" + }, + { + "name": "isPnhEnabledChat", + "write": "constructor" + }, + { + "name": "isUsernameThread", + "write": "constructor" + }, + { + "name": "isUsernameThreadAtCreation", + "write": "constructor" + }, + { + "name": "messagesRead", + "write": "constructor" + }, + { + "name": "messagesReceived", + "write": "constructor" + }, + { + "name": "messagesSent", + "write": "constructor" + }, + { + "name": "messagesUnread", + "write": "constructor" + }, + { + "name": "oppositePartyHasProfilePhoto", + "write": "constructor" + }, + { + "name": "oppositeVisibleIdentification", + "write": "constructor" + }, + { + "name": "profileReplies", + "write": "constructor" + }, + { + "name": "profileViews", + "write": "constructor" + }, + { + "name": "reactionsReceived", + "write": "constructor" + }, + { + "name": "reactionsSent", + "write": "constructor" + }, + { + "name": "repliesSent", + "write": "constructor" + }, + { + "name": "sharesCommonGroup", + "write": "constructor" + }, + { + "name": "statusReactionsReceived", + "write": "constructor" + }, + { + "name": "statusReactionsSent", + "write": "constructor" + }, + { + "name": "statusReplies", + "write": "constructor" + }, + { + "name": "statusViews", + "write": "constructor" + }, + { + "name": "threadDs", + "write": "constructor" + }, + { + "name": "threadId", + "write": "constructor" + }, + { + "name": "typeOfGroup", + "write": "constructor" + }, + { + "name": "viewOnceMessagesOpened", + "write": "constructor" + }, + { + "name": "viewOnceMessagesReceived", + "write": "constructor" + }, + { + "name": "viewOnceMessagesSent", + "write": "constructor" + } + ] } ] }, @@ -61534,7 +62139,16 @@ "callSites": [ { "module": "WAWebThreadLoggingIntegrity", - "partial": true + "fields": [ + { + "name": "threadDs", + "write": "constructor" + }, + { + "name": "threadId", + "write": "constructor" + } + ] } ] }, @@ -62869,7 +63483,25 @@ "callSites": [ { "module": "WAWebStatusSubtitle.react", - "partial": true + "fields": [ + { + "name": "attributionType", + "write": "constructor" + }, + { + "name": "statusCategory", + "write": "constructor" + }, + { + "name": "viewerActionType", + "write": "constructor", + "value": { + "kind": "enumMember", + "module": "WAWebWamEnumStatusViewActionType", + "key": "ATTRIBUTION_TAPPED" + } + } + ] }, { "module": "WAWebStatusSubtitle.react", @@ -70146,7 +70778,52 @@ "callSites": [ { "module": "WAWebSmb3pdAggregatedConversionSignalAction", - "partial": true + "fields": [ + { + "name": "adId", + "write": "constructor" + }, + { + "name": "ctwa3pdAggregatedSchemaVersion", + "write": "constructor" + }, + { + "name": "ctwa3pdConversionSubtype", + "write": "constructor" + }, + { + "name": "ctwa3pdConversionType", + "write": "constructor" + }, + { + "name": "ctwa3pdSurfaceType", + "write": "constructor", + "value": { + "kind": "str", + "value": "thread" + } + }, + { + "name": "ctwaConversationDepth", + "write": "constructor" + }, + { + "name": "ctwaConversationRepeat", + "write": "constructor" + }, + { + "name": "ctwaDirectionFrom", + "write": "constructor" + }, + { + "name": "ctwaSignals", + "write": "constructor" + }, + { + "name": "threadIdHmac", + "write": "constructor" + } + ] } ] }, From 9e609d4dee4c80ec50bc42a75110e69db13400d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:31:59 -0300 Subject: [PATCH 16/52] fix(codegen): reject binary content in optional child error arms --- crates/wa-codegen/src/spec.rs | 6 +++ .../wa-codegen/tests/fixtures/iq_runtime.rs | 43 +++++++++++++++++++ docs/iq-response-admission.md | 12 ++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/crates/wa-codegen/src/spec.rs b/crates/wa-codegen/src/spec.rs index e76c496e..044144b8 100644 --- a/crates/wa-codegen/src/spec.rs +++ b/crates/wa-codegen/src/spec.rs @@ -463,6 +463,12 @@ fn emit_direct_error_parser( .cloned() .collect(); let mut conditions = vec![error_arm_condition(arm)]; + // optionalChildWithTag calls maybeChildren before looking up a tag; + // any binary body (including empty bytes) fails this arm. Attribute-only + // arms do not inspect content and must remain eligible for fallback. + if fields.iter().any(|f| f.method == "child") { + conditions.push("error.content_bytes().is_none()".to_string()); + } for child in fields.iter().filter(|f| f.method == "child") { conditions.push(format!( "error.get_children_by_tag({}).count() <= 1", diff --git a/crates/wa-codegen/tests/fixtures/iq_runtime.rs b/crates/wa-codegen/tests/fixtures/iq_runtime.rs index 5b67f418..2fa520b0 100644 --- a/crates/wa-codegen/tests/fixtures/iq_runtime.rs +++ b/crates/wa-codegen/tests/fixtures/iq_runtime.rs @@ -52,6 +52,9 @@ impl<'a> NodeRef<'a> { .filter(move |n| n.tag == tag) .map(Node::as_ref) } + pub fn content_bytes(&self) -> Option<&'a [u8]> { + self.node.bytes.as_deref() + } pub fn content_str(&self) -> Option<&'a str> { self.node .bytes @@ -341,6 +344,46 @@ fn main() { _ => panic!("unexpected error arm"), } } + // optionalChildWithTag first calls maybeChildren: binary content fails + // even when empty. The surrounding disjunction then reaches fallback. + for bytes in [vec![], b"opaque".to_vec(), vec![0xff]] { + let mut node = error("406", Some("not-acceptable")); + node.children[0].bytes = Some(bytes.clone()); + let MakeSetSubjectRequestResponse::ClientError(value) = subject + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap() + else { + panic!("expected client error"); + }; + assert!(matches!( + value.error_set_subject_client_errors.unwrap(), + MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorFallbackClient(_) + )); + + // Attribute-only error arms do not call maybeChildren. Their binary + // body must not trigger a blanket rejection or forced fallback. + let mut node = error("400", Some("bad-request")); + node.children[0].bytes = Some(bytes.clone()); + let MakeSetSubjectRequestResponse::ClientError(value) = subject + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap() + else { + panic!("expected client error"); + }; + assert!(matches!( + value.error_set_subject_client_errors.unwrap(), + MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorBadRequest(_) + )); + + let mut node = error("500", Some("resource-constraint")); + node.children[0].bytes = Some(bytes); + assert!(matches!( + accept + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakeAcceptGroupAddRequestResponse::ClientError(_) + )); + } for wire in ["id", "from"] { let mut invalid = bare.clone(); invalid.attrs.remove(wire); diff --git a/docs/iq-response-admission.md b/docs/iq-response-admission.md index 182ea9fb..035a85e8 100644 --- a/docs/iq-response-admission.md +++ b/docs/iq-response-admission.md @@ -94,6 +94,12 @@ continue to bare success in source order. For SetSubject 406/`not-acceptable`, a `` makes the specific parser fail; the source disjunction then tries the 400–499 fallback, which succeeds. Code/text selection is not a commit point. The compiled regression checks absent, valid, malformed and duplicated children. +`optionalChildWithTag` calls `maybeChildren` first, which rejects binary content +(including an empty byte array). A binary body on the 406/`not-acceptable` error +therefore reaches fallback too. The guard applies only to arms inspecting +children: attribute-only 400/`bad-request` and 500/`resource-constraint` retain +their specific outcomes with binary bodies. Empty, UTF-8 and non-UTF-8 byte +bodies are covered by the compiled regression. Unknown unrelated children remain accepted. This is not general JavaScript evaluation or a universal guard AST. ### Source provenance @@ -151,9 +157,9 @@ context. Copying this test to the first fail-closed commit reproduces the missin context API failure. The adapter does not qualify a binary codec or a downstream client; independent conformance remains separately owned. -Two debug generations of all 142 IQ operations produced identical 2,061,400-byte -outputs, SHA-256 `7443c11931dbfa69701838748101acf35982ebd1e4d8e22063aef4908632faf7`, -in 2.86 and 2.88 seconds, with 39 explicitly rejected parsers. The original base +Two debug generations of all 142 IQ operations produced identical 2,061,750-byte +outputs, SHA-256 `006b5a5e392892b4cb89f98274bf3eed171d66179f752c83bb491ee387de3d71`, +in 3.03 and 3.00 seconds, with 39 explicitly rejected parsers. The original base produced 1,523,859 bytes in 2.84 and 2.98 seconds. The increase includes recovered outcome types and context-taking parsers; it is not an optimization claim. The Rust reference catalog remains ignored by git. Maintenance requires review From 726d59eb1d87cb1a03765d7edac3f64956f2eb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:33:27 +0000 Subject: [PATCH 17/52] test: repin snapshot gaps after source-level attribution --- docs/snapshot-quality-2026-10-07.gaps.json | 422 +++++ docs/snapshot-quality-2026-10-07.md | 282 +-- docs/snapshot-quality-2026-10-07.sources.json | 1614 ++++++++++++++++- docs/snapshot-quality.md | 21 +- scripts/lint-ir.py | 32 +- scripts/tests/test_snapshot_baselines.py | 112 ++ 6 files changed, 2279 insertions(+), 204 deletions(-) create mode 100644 docs/snapshot-quality-2026-10-07.gaps.json create mode 100644 scripts/tests/test_snapshot_baselines.py diff --git a/docs/snapshot-quality-2026-10-07.gaps.json b/docs/snapshot-quality-2026-10-07.gaps.json new file mode 100644 index 00000000..c1a42c0a --- /dev/null +++ b/docs/snapshot-quality-2026-10-07.gaps.json @@ -0,0 +1,422 @@ +{ + "evidenceVersion": 1, + "scope": "Static source and extraction observations, not server behavior or guaranteed rename identities.", + "snapshots": { + "old": { + "waVersion": "2.3000.1045368834", + "bundleCount": 516, + "setHash": "99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00", + "coverage": "parsed-static-definitions" + }, + "new": { + "waVersion": "2.3000.1047483476", + "bundleCount": 579, + "setHash": "05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b", + "coverage": "parsed-static-definitions" + } + }, + "wam": { + "originalCounters": { + "old": { + "construction of an event with no catalog entry": 41, + "unreadConstructionArgument.call": 1, + "unreadConstructionArgument.identifier": 100, + "unreadConstructionArgument.member": 3 + }, + "new": { + "construction of an event with no catalog entry": 29, + "unreadConstructionArgument.call": 1, + "unreadConstructionArgument.identifier": 89, + "unreadConstructionArgument.member": 3 + } + }, + "fixedCounters": { + "old": { + "construction of an event with no catalog entry": 41, + "unreadConstructionArgument.call": 1, + "unreadConstructionArgument.identifier": 86, + "unreadConstructionArgument.member": 3 + }, + "new": { + "construction of an event with no catalog entry": 29, + "unreadConstructionArgument.call": 1, + "unreadConstructionArgument.identifier": 75, + "unreadConstructionArgument.member": 3 + } + }, + "bridgeModuleDepartures": [ + "WAWebWindowsHybridBridgeWam.v2589", + "WAWebWindowsHybridBridgeWam.v2590", + "WAWebWindowsHybridBridgeWam.v2591", + "WAWebWindowsHybridBridgeWam.v2601", + "WAWebWindowsHybridBridgeWam.v2602", + "WAWebWindowsHybridBridgeWam.v2603", + "WAWebWindowsHybridBridgeWam.v2604", + "WAWebWindowsHybridBridgeWam.v2608", + "WAWebWindowsHybridBridgeWam.v2609", + "WAWebWindowsHybridBridgeWam.v2610", + "WAWebWindowsHybridBridgeWam.v2617", + "WAWebWindowsHybridBridgeWam.v2618", + "WAWebWindowsHybridBridgeWam.v2619", + "WAWebWindowsHybridBridgeWam.v2621", + "WAWebWindowsHybridBridgeWam.v2624" + ], + "bridgeModuleAdditions": [ + "WAWebWindowsHybridBridgeWam.v2634", + "WAWebWindowsHybridBridgeWam.v2635", + "WAWebWindowsHybridBridgeWam.v2636" + ], + "bridgeReason": "Each exact module has one generic RawWamEvent construction with an unread runtime object. Complete parse coverage proves these static definitions depart/arrive, not feature removal.", + "newUnreadArgumentBeforeFix": "WAWebStatusSubtitle.react", + "recoveredWithLocalObjectFix": { + "old": [ + { + "constructionSha256": "074551c9a63866a6e33995c969fbb7249ddb191b34ff7cefa1b6c6d17d596eb7", + "end": 402, + "eventExport": "ThreadInteractionDataAiWamEvent", + "eventModule": "WAWebThreadInteractionDataAiWamEvent", + "module": "WAWebThreadLoggingAi", + "moduleSha256": "b145e75b68163c093af2109db48aab201cb09b58bf6117e2903e424b33d42720", + "reason": "unreadConstructionArgument.identifier", + "start": 321, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebThreadLoggingAi" + }, + { + "constructionSha256": "18d20452c8692bc73b60abf0acf82dc24a0cbbdd20fc770eb0977f6ae632b5e2", + "end": 7144, + "eventExport": "ChatFilterEventWamEvent", + "eventModule": "WAWebChatFilterEventWamEvent", + "module": "WAWebFilterLogging", + "moduleSha256": "0da3d9c081cbf3b3372af10aa04da2f768a58c316caf63676456e2104fbe7783", + "reason": "unreadConstructionArgument.identifier", + "start": 7079, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebFilterLogging" + }, + { + "constructionSha256": "1a6bebc04cfb9fddbd70c448eaf19fa2dfd7919df5219256602be7b1486c349b", + "end": 1137, + "eventExport": "MexEventV2WamEvent", + "eventModule": "WAWebMexEventV2WamEvent", + "module": "WAWebMexLogging", + "moduleSha256": "e6d862350fb96e5beb62395ed2f4742252b959cc7970a342b66b24c84e99b621", + "reason": "unreadConstructionArgument.identifier", + "start": 1082, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebMexLogging" + }, + { + "constructionSha256": "24a0165bb666db71ad908eed55707840333ad3932b2fb5ec26fb1e5c66b435ea", + "end": 450, + "eventExport": "PaymentsUserActionWamEvent", + "eventModule": "WAWebPaymentsUserActionWamEvent", + "module": "WAWebPaymentsUserActionLogging", + "moduleSha256": "725c3fec4cba9a60a82dcb9c2a433194b770beb121fe235bee5bd3a5d3b8d70f", + "reason": "unreadConstructionArgument.identifier", + "start": 379, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebPaymentsUserActionLogging" + }, + { + "constructionSha256": "24a0165bb666db71ad908eed55707840333ad3932b2fb5ec26fb1e5c66b435ea", + "end": 854, + "eventExport": "PaymentsUserActionWamEvent", + "eventModule": "WAWebPaymentsUserActionWamEvent", + "module": "WAWebPaymentsUserActionLogging", + "moduleSha256": "725c3fec4cba9a60a82dcb9c2a433194b770beb121fe235bee5bd3a5d3b8d70f", + "reason": "unreadConstructionArgument.identifier", + "start": 783, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebPaymentsUserActionLogging" + }, + { + "constructionSha256": "432c4b5c3405ea761b0ef66f632545ed9e3812b745a0ed8b2fae44d5a66a3b4e", + "end": 332, + "eventExport": "ThreadInteractionDataIntegrityWamEvent", + "eventModule": "WAWebThreadInteractionDataIntegrityWamEvent", + "module": "WAWebThreadLoggingIntegrity", + "moduleSha256": "35589ea4523873fe91790e9bd6f73589e1886b53eb5fc6f8f1a391d9d5b69531", + "reason": "unreadConstructionArgument.identifier", + "start": 237, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebThreadLoggingIntegrity" + }, + { + "constructionSha256": "63219d354832b0c9d52c6f8cda776751d8ff66a465fd941835ba5239db7083b3", + "end": 3084, + "eventExport": "ThreadInteractionDataCoreConsumerWamEvent", + "eventModule": "WAWebThreadInteractionDataCoreConsumerWamEvent", + "module": "WAWebThreadLoggingCoreConsumer", + "moduleSha256": "5a69f324b23edd70ceaa5fe6c1252f56a493b61fadb5f55f5764dd16c2d0661e", + "reason": "unreadConstructionArgument.identifier", + "start": 2983, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebThreadLoggingCoreConsumer" + }, + { + "constructionSha256": "64c650bc28604bac1e25891dabb740fff21e12d0d30f7eeb70dbc18b48a95ab1", + "end": 837, + "eventExport": "ThreadInteractionDataBizWamEvent", + "eventModule": "WAWebThreadInteractionDataBizWamEvent", + "module": "WAWebThreadLoggingBiz", + "moduleSha256": "c131f7f32bcb77192abfc0542045221c12f75e2da33c2594063d999105050d65", + "reason": "unreadConstructionArgument.identifier", + "start": 754, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebThreadLoggingBiz" + }, + { + "constructionSha256": "6b7a1d2b67c21a76ed60b5327c68513211be395dcc7152ef59f20894e231adf0", + "end": 937, + "eventExport": "PaymentsUserActionWamEvent", + "eventModule": "WAWebPaymentsUserActionWamEvent", + "module": "WAWebPixWamLogger", + "moduleSha256": "eb24e5bee24de5260c8b6bb504fdf8cf163d20a763f19fd6acf808d805966ad3", + "reason": "unreadConstructionArgument.identifier", + "start": 866, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebPixWamLogger" + }, + { + "constructionSha256": "70f605e3cdcca05956caeaa0cebf4fd1a705533b91945f8ff929a4a4602a6983", + "end": 2044, + "eventExport": "Ctwa3pdAggregatedConversionWamEvent", + "eventModule": "WAWebCtwa3pdAggregatedConversionWamEvent", + "module": "WAWebSmb3pdAggregatedConversionSignalAction", + "moduleSha256": "5ac74826aadb1c4e9aa1d9b02f5bdbc514eb95ff7b5104d38d29d0fefc3f9725", + "reason": "unreadConstructionArgument.identifier", + "start": 1955, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebSmb3pdAggregatedConversionSignalAction" + }, + { + "constructionSha256": "9613c96822ea8a8b5acd8334a3538b61bc38ce9ebc7e0898ec7e0fae152c5d4b", + "end": 2949, + "eventExport": "StickerSendWamEvent", + "eventModule": "WAWebStickerSendWamEvent", + "module": "WAWebSendStickerAction", + "moduleSha256": "52d2433b889bdfc527fcf4923a500be1b30648d6a7ffdc68f2f639179bfdfdeb", + "reason": "unreadConstructionArgument.identifier", + "start": 2892, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebSendStickerAction" + }, + { + "constructionSha256": "9a62d614b7b1ad5c4f3bc26bea79da24bc656d9ec2ac9052e044ce1e13cf521f", + "end": 1607, + "eventExport": "WebcFingerprintWamEvent", + "eventModule": "WAWebWebcFingerprintWamEvent", + "module": "WAWebWamFingerprintReporter", + "moduleSha256": "3a2452fcfc9277edf7761595c092341f3ca70594087fbce930759a9322854972", + "reason": "unreadConstructionArgument.identifier", + "start": 1542, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebWamFingerprintReporter" + }, + { + "constructionSha256": "e7f3ca9f64782510d5ef70a38e262b5669c12b3a16c4f22c6689bcd93be32b9c", + "end": 381, + "eventExport": "ThreadInteractionDataNotificationWamEvent", + "eventModule": "WAWebThreadInteractionDataNotificationWamEvent", + "module": "WAWebThreadLoggingNotification", + "moduleSha256": "33b01e6054497198f4b082a7a7b63724d26ae37eb0cbdf16941ebf46a52fc5d9", + "reason": "unreadConstructionArgument.identifier", + "start": 280, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebThreadLoggingNotification" + }, + { + "constructionSha256": "f722bd819821b887453e0ac2c3eaba1b0961a54426e9443c55323927d1a661a4", + "end": 432, + "eventExport": "ThreadInteractionDataVoipWamEvent", + "eventModule": "WAWebThreadInteractionDataVoipWamEvent", + "module": "WAWebThreadLoggingVoip", + "moduleSha256": "524103e748da6c43badf73f0062a2b873b1042822a04f95671e6a325f716fdef", + "reason": "unreadConstructionArgument.identifier", + "start": 347, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/old/modules/WAWebThreadLoggingVoip" + } + ], + "new": [ + { + "constructionSha256": "074551c9a63866a6e33995c969fbb7249ddb191b34ff7cefa1b6c6d17d596eb7", + "end": 402, + "eventExport": "ThreadInteractionDataAiWamEvent", + "eventModule": "WAWebThreadInteractionDataAiWamEvent", + "module": "WAWebThreadLoggingAi", + "moduleSha256": "b145e75b68163c093af2109db48aab201cb09b58bf6117e2903e424b33d42720", + "reason": "unreadConstructionArgument.identifier", + "start": 321, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebThreadLoggingAi" + }, + { + "constructionSha256": "18d20452c8692bc73b60abf0acf82dc24a0cbbdd20fc770eb0977f6ae632b5e2", + "end": 7189, + "eventExport": "ChatFilterEventWamEvent", + "eventModule": "WAWebChatFilterEventWamEvent", + "module": "WAWebFilterLogging", + "moduleSha256": "85cbd606e0574186e121e596c1470e1f97ec3afe3fee67bdce69d9e309082669", + "reason": "unreadConstructionArgument.identifier", + "start": 7124, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebFilterLogging" + }, + { + "constructionSha256": "1a6bebc04cfb9fddbd70c448eaf19fa2dfd7919df5219256602be7b1486c349b", + "end": 1137, + "eventExport": "MexEventV2WamEvent", + "eventModule": "WAWebMexEventV2WamEvent", + "module": "WAWebMexLogging", + "moduleSha256": "e6d862350fb96e5beb62395ed2f4742252b959cc7970a342b66b24c84e99b621", + "reason": "unreadConstructionArgument.identifier", + "start": 1082, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebMexLogging" + }, + { + "constructionSha256": "24a0165bb666db71ad908eed55707840333ad3932b2fb5ec26fb1e5c66b435ea", + "end": 450, + "eventExport": "PaymentsUserActionWamEvent", + "eventModule": "WAWebPaymentsUserActionWamEvent", + "module": "WAWebPaymentsUserActionLogging", + "moduleSha256": "725c3fec4cba9a60a82dcb9c2a433194b770beb121fe235bee5bd3a5d3b8d70f", + "reason": "unreadConstructionArgument.identifier", + "start": 379, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebPaymentsUserActionLogging" + }, + { + "constructionSha256": "24a0165bb666db71ad908eed55707840333ad3932b2fb5ec26fb1e5c66b435ea", + "end": 854, + "eventExport": "PaymentsUserActionWamEvent", + "eventModule": "WAWebPaymentsUserActionWamEvent", + "module": "WAWebPaymentsUserActionLogging", + "moduleSha256": "725c3fec4cba9a60a82dcb9c2a433194b770beb121fe235bee5bd3a5d3b8d70f", + "reason": "unreadConstructionArgument.identifier", + "start": 783, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebPaymentsUserActionLogging" + }, + { + "constructionSha256": "432c4b5c3405ea761b0ef66f632545ed9e3812b745a0ed8b2fae44d5a66a3b4e", + "end": 332, + "eventExport": "ThreadInteractionDataIntegrityWamEvent", + "eventModule": "WAWebThreadInteractionDataIntegrityWamEvent", + "module": "WAWebThreadLoggingIntegrity", + "moduleSha256": "35589ea4523873fe91790e9bd6f73589e1886b53eb5fc6f8f1a391d9d5b69531", + "reason": "unreadConstructionArgument.identifier", + "start": 237, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebThreadLoggingIntegrity" + }, + { + "constructionSha256": "575f8ae4397f34807d6b782bd9be69a62dc3adfc31edbe5f792f62bada8f57d7", + "end": 2873, + "eventExport": "StatusViewerActionWamEvent", + "eventModule": "WAWebStatusViewerActionWamEvent", + "module": "WAWebStatusSubtitle.react", + "moduleSha256": "e3bd54e8504c4749609e0d99d9ed4f425065deb6ccc7a98a6a0c1ed24f91a591", + "reason": "unreadConstructionArgument.identifier", + "start": 2802, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebStatusSubtitle.react" + }, + { + "constructionSha256": "63219d354832b0c9d52c6f8cda776751d8ff66a465fd941835ba5239db7083b3", + "end": 3084, + "eventExport": "ThreadInteractionDataCoreConsumerWamEvent", + "eventModule": "WAWebThreadInteractionDataCoreConsumerWamEvent", + "module": "WAWebThreadLoggingCoreConsumer", + "moduleSha256": "5a69f324b23edd70ceaa5fe6c1252f56a493b61fadb5f55f5764dd16c2d0661e", + "reason": "unreadConstructionArgument.identifier", + "start": 2983, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebThreadLoggingCoreConsumer" + }, + { + "constructionSha256": "64c650bc28604bac1e25891dabb740fff21e12d0d30f7eeb70dbc18b48a95ab1", + "end": 837, + "eventExport": "ThreadInteractionDataBizWamEvent", + "eventModule": "WAWebThreadInteractionDataBizWamEvent", + "module": "WAWebThreadLoggingBiz", + "moduleSha256": "c131f7f32bcb77192abfc0542045221c12f75e2da33c2594063d999105050d65", + "reason": "unreadConstructionArgument.identifier", + "start": 754, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebThreadLoggingBiz" + }, + { + "constructionSha256": "70f605e3cdcca05956caeaa0cebf4fd1a705533b91945f8ff929a4a4602a6983", + "end": 2044, + "eventExport": "Ctwa3pdAggregatedConversionWamEvent", + "eventModule": "WAWebCtwa3pdAggregatedConversionWamEvent", + "module": "WAWebSmb3pdAggregatedConversionSignalAction", + "moduleSha256": "5ac74826aadb1c4e9aa1d9b02f5bdbc514eb95ff7b5104d38d29d0fefc3f9725", + "reason": "unreadConstructionArgument.identifier", + "start": 1955, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebSmb3pdAggregatedConversionSignalAction" + }, + { + "constructionSha256": "9613c96822ea8a8b5acd8334a3538b61bc38ce9ebc7e0898ec7e0fae152c5d4b", + "end": 2987, + "eventExport": "StickerSendWamEvent", + "eventModule": "WAWebStickerSendWamEvent", + "module": "WAWebSendStickerAction", + "moduleSha256": "9357b5727a22496e1630d342ca87220f8288c6e18bf81a98a8f4db05e47db6a5", + "reason": "unreadConstructionArgument.identifier", + "start": 2930, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebSendStickerAction" + }, + { + "constructionSha256": "9a62d614b7b1ad5c4f3bc26bea79da24bc656d9ec2ac9052e044ce1e13cf521f", + "end": 1607, + "eventExport": "WebcFingerprintWamEvent", + "eventModule": "WAWebWebcFingerprintWamEvent", + "module": "WAWebWamFingerprintReporter", + "moduleSha256": "3a2452fcfc9277edf7761595c092341f3ca70594087fbce930759a9322854972", + "reason": "unreadConstructionArgument.identifier", + "start": 1542, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebWamFingerprintReporter" + }, + { + "constructionSha256": "e7f3ca9f64782510d5ef70a38e262b5669c12b3a16c4f22c6689bcd93be32b9c", + "end": 381, + "eventExport": "ThreadInteractionDataNotificationWamEvent", + "eventModule": "WAWebThreadInteractionDataNotificationWamEvent", + "module": "WAWebThreadLoggingNotification", + "moduleSha256": "33b01e6054497198f4b082a7a7b63724d26ae37eb0cbdf16941ebf46a52fc5d9", + "reason": "unreadConstructionArgument.identifier", + "start": 280, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebThreadLoggingNotification" + }, + { + "constructionSha256": "f722bd819821b887453e0ac2c3eaba1b0961a54426e9443c55323927d1a661a4", + "end": 432, + "eventExport": "ThreadInteractionDataVoipWamEvent", + "eventModule": "WAWebThreadInteractionDataVoipWamEvent", + "module": "WAWebThreadLoggingVoip", + "moduleSha256": "524103e748da6c43badf73f0062a2b873b1042822a04f95671e6a325f716fdef", + "reason": "unreadConstructionArgument.identifier", + "start": 347, + "sourceReference": "snapshot-quality-2026-10-07.sources.json#/new/modules/WAWebThreadLoggingVoip" + } + ] + } + }, + "mex": { + "departures": [ + "CreateLabyrinthBackup", + "DebugLabyrinthInboxSnapshot", + "DebugLabyrinthRange", + "EBMessageMetadataQuery", + "RotateLabyrinthEpoch", + "TeamLinkCreateInvitation", + "TeamLinkListInvitations", + "TeamLinkRemoveInvitation", + "UploadLabyrinthMessages" + ], + "additions": [ + "AccountLinkingAPIGetCerts", + "UpdateNewsletterAdminProfileSetting", + "useWAWebSmartComposerCoachSuggestedReply" + ], + "presenceArithmetic": { + "removedUndeterminedKeys": 6, + "removedWhollyUndeterminedOperations": 4, + "newContactManagerUndeterminedKeys": 3, + "newSmartComposerUndeterminedKeys": 1, + "newSmartComposerWhollyUndeterminedOperations": 1 + }, + "remainingLimits": { + "ContactManagerCustomerProfiles": "filters .map return and nested presence remain undetermined; candidate_lids conditional/string inference is not a proven upstream type change.", + "useWAWebSmartComposerCoachSuggestedReply": "The call uses a mutation function obtained from CometRelay.useMutation(...)[0], not a recovered direct Relay fetch. The fallback input shape/presence does not recover visible nested input fields." + } + }, + "iq": { + "attributeArithmetic": "55 - 3 privacy recovered - 3 absent KMP module = 49", + "childArithmetic": "12 - 1 privacy recovered - 1 absent KMP module = 10", + "contentArithmetic": "23 - 1 absent KMP module + 1 SetAbout positional entry = 23", + "moduleDeparture": "WAWebKmpSyncdRequestBuilder", + "limits": "Exact static module absence only; no assertion that the wire operation vanished or was renamed." + } +} diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index dca6c104..f1336f69 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -1,11 +1,10 @@ -# September snapshot quality investigation +# Snapshot quality investigation — 2026-10-07 -The current remote HEAD checked on 2026-10-07 was -`1f5167c4cebf263edaf001e06a2497f213c55793`. The comparison baseline was -`1a441f0329c941fcdb238490a6c604550d8a9939`. The intervening commit changes only -16 generated files. Both use IR schema 4.3.0 and the same extractor source. -The reference research report supplied through Library was treated as a set of -claims to recheck. No other repository or real session was used. +Remote HEAD at the initial check was `1f5167c4cebf263edaf001e06a2497f213c55793`; +the prior snapshot was `1a441f0329c941fcdb238490a6c604550d8a9939`. Their difference +contains only 16 generated files, with the same extractor and IR schema 4.3.0. +The Library research report supplied for this investigation was treated as claims +to verify. No other repository or real session was used. | Evidence | Old | New | |---|---|---| @@ -14,120 +13,163 @@ claims to recheck. No other repository or real session was used. | Set hash | `99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00` | `05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b` | | Archive SHA-256 | `2edd4b3f8dae50b503e0b15920adc774e2680cdb542396d57608bb0374196ee1` | `22243b9c2ff18a66cea8fd66551821fe0f9f09786d107e2be8284637db86c349` | -Both archives came from `oxidezap/whatspec` release `bundle-store`, named -`bundles--.tar.xz`. Restore verified each exact bundle -multiset. TLS in the Rust downloader reported `UnknownIssuer`; `curl` downloaded -the same assets using the environment trust store, without disabling TLS checks. -Restore then consumed the local archives. This is independent of the `gh` API -requests that returned `Forbidden`; the connected GitHub search returned no open -PRs at the initial check. - -## Reproduced checks - -All 12 real JSON documents in each snapshot pass their emitted schemas with -`jsonschema 4.26.0`. The validator previously returned success for an entirely -empty directory, printing 12 `skip` lines. It now fails missing documents, -missing schemas, malformed JSON, invalid schemas, and unresolved external -references. Nine offline regression tests cover those cases, valid input, and an explicit -count of successfully validated pairs. - -Both snapshots reproduce all 27 checked artifacts with the same release binary -and `update --bundles ... --wa-version ... --check`. The old run took 13.970 s -with peak RSS 2,387,992 KiB; the new run took 13.114 s with peak RSS 2,386,528 KiB. -These are single warm runs measured with Python monotonic time and child-process -resource usage, not benchmarks. They prove reproduction, not protocol completeness. - -The lint result still fails six exact baselines. No limit was relaxed. Its old -`IMPROVED ... lower the baseline` diagnosis was unsound without source review; -it now reports a changed state and requests investigation. - -| Counted gap | Old baseline | New observed | Attribution | -|---|---:|---:|---| -| IQ attributes without argument paths | 55 | 49 | Privacy recovers 3; KMP catalog departure removes 3 | -| IQ children without argument paths | 12 | 10 | Privacy recovers 1; KMP catalog departure removes 1 | -| MEX undetermined variable presence | 108 | 106 | Removed operations subtract 6; ContactManager adds 3; SmartComposer adds 1 | -| MEX operations with no established presence | 13 | 10 | Four removed operations; one new SmartComposer operation | -| WAM uncataloged-event constructions | 41 | 29 | Indeterminate without per-construction attribution | -| WAM unread construction arguments | 104 | 93 | Identifier arguments 100→89; call 1 and member 3 unchanged; cause indeterminate | - -The IQ missing-content count stays at 23 through cancellation: KMP departure -subtracts one while SetAbout's changed selected entry point adds one. An unchanged -total therefore also needs identity-level review. - -## Source findings - -The compact [source sidecar](snapshot-quality-2026-10-07.sources.json) records -recoverable bundle hashes and AST byte spans for the findings below. All source -inspection used those AST ranges; no `eval`, `vm`, or authenticated client ran. - -`WAWebSetPrivacyJob` changes helper `f` from four positional parameters to an -object with `dhash`, `name`, `users`, and `value`. Both versions still construct -`privacy/category` and repeated `user` nodes. The new IR recovers the three -category argument paths and the child's `users[]` path. It also corrects the old -`name/action` and `name/wid` paths to `users[]/action` and `users[]/wid`. -This is recovered extraction quality enabled by an upstream refactor, not a -wire feature removal or proof that all branches are modeled. The old source is -bundle `fd26924373be0ce0e773c304511df62909fa76e11d9294c0509542d3f38f842d`, -bytes 1087286..1090426; the new source is -`7c7be0378ce2e7ff49b9f5909cf8a7d283000af82cc43bc071c2df3541ec5d56`, -bytes 183590..186813. - -`WAWebKmpSyncdRequestBuilder` contributed three unaddressed attributes, one child, -and one content value. It is present in the old AST index and not recovered in -the new one. This explains the counter arithmetic but does not establish removal -of app-state protocol support. The source-index API does not report parse errors, -and a renamed or relocated implementation needs separate investigation. Keep -this catalog departure indeterminate. - -`WAWebSetAboutJob` is not a proven rename from `setAbout` to `sendSetAbout`. -The new source exports both. It moves the IQ construction into a positional -`sendSetAbout` function, called from the persisted job. The selected IR export -changes, losing the old `content` argument-object path while retaining dynamic -status content. Treat the add/remove pair separately; consumers must not assume -that matching wire shapes establish function identity. - -Nine MEX operation names depart and three arrive. The departures are -CreateLabyrinthBackup, DebugLabyrinthInboxSnapshot, DebugLabyrinthRange, -EBMessageMetadataQuery, RotateLabyrinthEpoch, TeamLinkCreateInvitation, -TeamLinkListInvitations, TeamLinkRemoveInvitation, and UploadLabyrinthMessages. -The additions are AccountLinkingAPIGetCerts, UpdateNewsletterAdminProfileSetting, -and useWAWebSmartComposerCoachSuggestedReply. Four departures explain six fewer -undetermined keys and four fewer wholly undetermined operations. The new -SmartComposer operation adds one of each. - -ContactManagerCustomerProfiles adds three undetermined presence nodes under -`filters`, accounting for the remainder of the MEX counter change. Its source -now constructs a filters array with `.map`. The operation's persisted ID change -is independently proved by the literal exports in its facebookRelayOperation -module: `27747880408206174` becomes `27796221486653417`. Other changes, including -candidate_lids' shape becoming a string and presence becoming conditional, -remain unclassified pending a focused MEX inference review. An aggregate drop -must not hide those new gaps. - -WAM changes from 883 to 874 constructions, 807 to 810 call sites, and 121 to 126 -partial sites. These aggregates do not identify the 12 uncataloged-event and -11 unread-argument departures. Neither the lower gap counts nor the higher -call-site count justifies lowering the baselines before per-site attribution. - -## Review artifact and remaining work - -The generic report contains 472 deltas for this pair, including one opaque -protobuf artifact delta. The checked-in [review ledger](snapshot-quality-2026-10-07.reviews.json) -classifies the privacy improvement and the ContactManager persisted-ID change. -The remaining 470 deltas stay indeterminate; unreviewed is not equivalent to bad. -Generate the report with: +Both archives came from this repository's `bundle-store` release. Restore checked +the exact bundle multiset, sizes, hashes and set hash. Rust TLS reported +`UnknownIssuer`; `curl` downloaded the same assets using the environment trust +store without disabling TLS checks, then restore consumed the local archives. +The `gh` API returned `Forbidden`; connected GitHub tools handled PR operations. + +## Checks and baseline dispositions + +Both historical snapshots pass all 12 emitted JSON schemas. The old validator +incorrectly passed an empty directory; it now fails missing documents/schemas, +invalid JSON/schema, and external references. It reports the number actually +validated. Nine validator regression tests cover those cases. + +Before the WAM extraction fix, the same release binary reproduced all 27 checked +artifacts of both historical commits. Warm runs took 13.970 s / 13.114 s, with +peak RSS 2,387,992 / 2,386,528 KiB. These are single observations, not benchmarks. + +All six failing count baselines have source-level dispositions below. They remain +exact pins in both directions, not upper bounds. Four additional Python tests +run lint on the real snapshot and mutate every repinned state upward/downward +(12 mutations), proving that neither an increase nor an unexplained decrease is +silently accepted. Schema conformance and lint are separate checks. + +| Counted gap | Historical old | Historical new | After fix / new pin | Disposition | +|---|---:|---:|---:|---| +| IQ attributes without argument paths | 55 | 49 | 49 | Privacy recovers 3; absent KMP definition removes 3 | +| IQ children without argument paths | 12 | 10 | 10 | Privacy recovers 1; absent KMP definition removes 1 | +| MEX undetermined presence nodes | 108 | 106 | 106 | Departing operations subtract 6; ContactManager adds 3; SmartComposer adds 1 | +| MEX operations with no established presence | 13 | 10 | 10 | Four departed operations; one new, unresolved SmartComposer operation | +| WAM uncataloged constructions | 41 | 29 | 29 | 15 old Windows bridge definitions depart; 3 arrive | +| WAM unread arguments | 104 | 93 | 79 | Bridge change −12; StatusSubtitle regression +1; local-object fix −14 | + +The [gap ledger](snapshot-quality-2026-10-07.gaps.json) records exact module names, +constructor spans/hashes, counter arithmetic, and remaining limitations. The +[source sidecar](snapshot-quality-2026-10-07.sources.json) locates the selected +modules in both locked sets. Every bundle parses without recovery: 20,209 old and +19,713 new exact static module names. This establishes coverage of static module +definitions, not completeness of the server protocol or dynamic JavaScript. + +## IQ evidence + +`WAWebSetPrivacyJob` changes its helper from four positional parameters to an +object containing `dhash`, `name`, `users`, and `value`. Both sources construct +`privacy/category` and repeated `user` nodes. The new IR recovers three category +argument paths and the child's `users[]` path. It also corrects old `name/action` +and `name/wid` paths to `users[]/action` and `users[]/wid`. This is improved +extraction enabled by an upstream refactor, not a wire feature removal. + +`WAWebKmpSyncdRequestBuilder` contributes three missing attribute paths, one child +path, and one content path in the old snapshot. Its exact static definition is +absent from the fully parsed new set. That catalog departure explains the +counter decrease. It does not establish that app-state support vanished, or that +another module is a rename. The disposition is upstream static-definition +change; the fate of the wire operation remains indeterminate. + +The unchanged missing-content total of 23 hides cancellation: KMP contributes +−1 and SetAbout contributes +1. `WAWebSetAboutJob` now exports both `setAbout` and +`sendSetAbout`, with the construction in a positional `sendSetAbout` function. +The selected IR entry loses its argument-object content path. The source no +longer supplies such an object at that entry. Treat the add/remove pair separately, +without a guaranteed rename or a promise of source API compatibility. + +## MEX evidence and retained gaps + +Nine operation names depart and three arrive; their exact names are in the gap +ledger. The corresponding departed `.graphql` definitions are absent from the +fully parsed new set. Four removed operations account for six undetermined keys +and four wholly undetermined operations. These are catalog departures, not proof +of server-side operation retirement. + +`WAWebContactManagerCustomerProfilesQuery` adds `filters`, built through `.map`, +and two nested field names. Their presence stays undetermined because the current +classifier does not establish the call's return semantics. The source adds these +fields; this is a new unresolved surface, not silently discarded extraction. +Its `candidate_lids` now uses a nullish fallback to `[]`; the IR's string/conditional +inference is **not** a proved upstream type change and remains indeterminate. +A method named `map` alone does not guarantee Array semantics. These limitations +are preserved explicitly rather than claiming a complete variable contract. + +The new SmartComposer operation takes a mutation function from +`CometRelay.useMutation(...)[0]` and later calls it with a nested `variables.input` +object. The extractor does not resolve that alias; it falls back to the compiled +GraphQL `input` argument with undetermined presence. The visible nested fields +remain unrecovered. This is a new unsupported call shape, recorded as a gap; it +adds one node and one wholly undetermined operation. The baseline review does +not certify that fallback as complete or infer those missing fields. + +The ContactManager persisted-ID change is independently proven by literal +facebookRelayOperation exports: `27747880408206174` → `27796221486653417`. + +## WAM evidence and extraction repair + +Each departed/added `WAWebWindowsHybridBridgeWam.v*` definition constructs one +`RawWamEvent` from a runtime object originating in native JSON. That schema cannot +be recovered as a fixed catalog event. The 15 departures and 3 arrivals explain +both the uncataloged count change (41→29) and −12 unread identifiers. Call (1) +and member (3) unread forms remain unchanged. + +There is also a genuine loss: `WAWebStatusSubtitle.react` moves the inline +attribution object into a local variable, preserving `attributionType`, +`statusCategory`, and `viewerActionType`. The old extractor turns the direct +construction into an empty partial site. Exact pinned module fixtures reproduce +that failure on provenance-only commit `4d1bed4`; the same test passes with the +local-object fix. A smaller synthetic refactor also fails before and passes after. + +Recovery accepts only a uniquely bound, directly initialized function-local +object, with all uses accounted for as recognized constructor reads or sources +of a fresh-target `babelHelpers.extends`. Mutation, escape, rebinding, conditional +initialization, pre-initialization reads, direct eval, shadowing and outer closure +reads remain unresolved. The pinned `WAWebWamTypeHash` constructor copies input +fields through `set`; it does not mutate the source object. The async closure's +inherited fields remain partial because invocation timing is not established. + +The fix recovers 14 unread arguments on the current set and 14 on the old set +(the memberships differ; the ledger lists both). On the current snapshot it adds +157 field occurrences, six field values, and two distinct call sites; partial +sites fall from 126 to 114. Remaining unread arguments are 75 identifiers, one +call and three members. Only `generated/wam/index.json` and +`generated/manifest.json` change. No IQ, `spec.rs`, or IR schema redesign occurs. +Existing extraction entry points and `WamDiagnostics` retain their API; the gap +sidecar uses a new optional function. IR schema stays 4.3.0 because this fills +existing field semantics and makes no consumer contract change. + +## Reproduction and maintenance + +At `4d1bed4`, `quality-gaps` emits the historical gap counts without the local +argument recovery. On the final revision it emits the repaired counts: ```sh -whatspec diff old-generated generated --json \ - --evidence docs/snapshot-quality-2026-10-07.reviews.json > contracts.json +whatspec source-index OLD_LOCK OLD_BUNDLES > old-sources.json +whatspec source-index NEW_LOCK NEW_BUNDLES > new-sources.json +whatspec quality-gaps OLD_LOCK OLD_BUNDLES > old-gaps.json +whatspec quality-gaps NEW_LOCK NEW_BUNDLES > new-gaps.json +whatspec update --bundles NEW_BUNDLES --wa-version 2.3000.1047483476 --check +python3 scripts/validate-schemas.py generated +python3 scripts/lint-ir.py generated +python3 -m unittest discover -s scripts/tests -v +cargo test -p wa-transform -p wa-wam -p whatspec --all-features ``` -The raw report is 2,591,393 bytes. Full optional source indexes are 14,248,640 and -14,223,916 bytes, with 20,209 and 19,713 recovered module names. Only the focused -sidecar and review ledger are committed. The generated IR is unchanged. - -Before the snapshot can pass lint, account for the KMP departure and WAM sites, -and review the new MEX gaps. Coordinate extractor or IQ IR corrections with -those owners. Shared workflows are unchanged; the conformity work can integrate -`python3 -m unittest discover -s scripts/tests -v`. CI must be checked on the -published SHA, and a failing baseline must remain visible until resolved. +Source indexes are generated on demand (~14.2 MB each), not committed wholesale. +The focused source and gap ledgers are ~66.6 KB and ~21.4 KB. Two real WAM fixtures +retain exact source slices, with bundle hashes and offsets in their headers. +The WAM document grows from 2,794,121 to 2,812,306 bytes by recovering existing +field semantics; optional provenance adds no per-field IR metadata. WAM gap evidence incurs extra parsing only +when requested. Final regeneration checks reproduce all 27 artifacts for the repaired current +snapshot and a separately regenerated old snapshot. Observed check times were +26.063 s (new) and 18.561 s (old); peak child RSS across the sequence was +2,390,792 KiB. Compilation overlapped these runs, so they are cost observations, +not an isolated performance comparison. + +Changes to snapshot membership require reviewing these ledgers; +count arithmetic alone is insufficient. + +The generic contract report contains 484 deltas (2,637,575 bytes), keeps ordered +arrays and does not infer renames. +The [review ledger](snapshot-quality-2026-10-07.reviews.json) classifies the privacy +improvement and persisted-ID change; the remaining 482 deltas stay indeterminate until +reviewed. `--evidence` binds each review to both artifact hashes. The baseline +dispositions above concern those six counters, not all contract differences. diff --git a/docs/snapshot-quality-2026-10-07.sources.json b/docs/snapshot-quality-2026-10-07.sources.json index 6f8a2e72..7ba66536 100644 --- a/docs/snapshot-quality-2026-10-07.sources.json +++ b/docs/snapshot-quality-2026-10-07.sources.json @@ -1,14 +1,66 @@ { "old": { "bundleCount": 516, - "coverage": "recovered-definitions-only", + "coverage": "parsed-static-definitions", "limits": [ "Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", - "The existing AST extractor does not expose parse diagnostics. Absence from this index is not proof of absence upstream.", + "Every bundle parsed without recovery. Absence only establishes that this exact static module name was not defined in these inputs; it does not prove feature removal or a rename.", "All recovered occurrences are retained. Equal module names with different hashes need review.", "Module provenance locates code; field-level semantics may require following dependencies." ], "modules": { + "EBMessageMetadataQuery": [ + { + "bundleSha256": "6fdb39e43e1ab6afd5cb4c6010459b6aae017c4a37dc3abc623fc1481c69c2fe", + "dependencies": [ + "EBAPIQPLPoints", + "EBAPIWorkerCheck", + "EBCreateGraphQLFetchFnUnified", + "EBMessageMetadataQueryQuery.graphql", + "EBMessageRangeQueryUtils", + "FBLogger", + "I64", + "LSDatabaseSingleton", + "QPLUserFlow", + "ReQL", + "WAHashStringToNumber", + "WAJids", + "WAResultOrError", + "XPlatRelayEnvironment", + "asyncToGeneratorRuntime", + "getErrorSafe", + "gkx", + "qpl", + "relay-runtime" + ], + "end": 160858, + "factorySha256": "fc8e5eff4724cd40715b3845cd79996a1eb3394c095048c92764c8583dd337da", + "moduleSha256": "7bece5d90ede769b990491a8c4d7aea94483c8fbbcaf9413710f65173793312d", + "start": 156055 + } + ], + "EBMessageMetadataQueryQuery.graphql": [ + { + "bundleSha256": "6fdb39e43e1ab6afd5cb4c6010459b6aae017c4a37dc3abc623fc1481c69c2fe", + "dependencies": [ + "EBMessageMetadataQueryQuery_facebookRelayOperation" + ], + "end": 143130, + "factorySha256": "7c82a71b1236de03778a5af52a7b4f55070b12a56533cab5339d096fab23d342", + "moduleSha256": "946aa76d3c7a9e5d5cb5ecb2e972c0c4d657b0a0a36f6be83984c98d744a96a8", + "start": 141159 + } + ], + "EBMessageMetadataQueryQuery_facebookRelayOperation": [ + { + "bundleSha256": "6fdb39e43e1ab6afd5cb4c6010459b6aae017c4a37dc3abc623fc1481c69c2fe", + "dependencies": [], + "end": 141157, + "factorySha256": "5849f6cac64604c988961da4a82a4409633f7ee9641e62d7ed29bacb45847888", + "moduleSha256": "3705571c702f947d37a6be56ba86c773e7b837cea11b25837c97909b920c2e9c", + "start": 141037 + } + ], "WAWebContactManagerCustomerProfilesQuery": [ { "bundleSha256": "792466f7a00cd5862ac782f916733975cd9f46800bb9fd9679a1301411c24508", @@ -39,6 +91,106 @@ "start": 78265 } ], + "WAWebCreateLabyrinthBackupJob": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [ + "WAWebCreateLabyrinthBackupJobMutation.graphql", + "WAWebLabyrinthDebugStateCache", + "WAWebRelayClient", + "asyncToGeneratorRuntime", + "err" + ], + "end": 247229, + "factorySha256": "58493ae306a051e746400bcf1a8b960c25c0bc1f521091fc133d6a10d487a169", + "moduleSha256": "79b8f05571ee12bdfaf532a969d1674dec530dd1a9970528a04dba3bd123b83a", + "start": 244242 + } + ], + "WAWebCreateLabyrinthBackupJobMutation.graphql": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [], + "end": 242979, + "factorySha256": "db0de46b3be930a317f5fa7d16e90e6043f2a2c3dd7e441c428920cdf580e8f3", + "moduleSha256": "b177cef8c23623a4b0c1309138fbe860373f4fcda69f518825e1472dc4a7458a", + "start": 241411 + } + ], + "WAWebDebugLabyrinthInboxSnapshotQuery": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [ + "WAWebDebugLabyrinthInboxSnapshotQuery.graphql", + "WAWebRelayClient", + "asyncToGeneratorRuntime" + ], + "end": 369270, + "factorySha256": "ce253946f67675cd71abcfe6e4be5679334ecf50b7ae3e5d92547f3559175acd", + "moduleSha256": "d07f137632a926aa7ddda9dbecc8fca9dbd39806a8f8a0009bd4fd4674f8de37", + "start": 368201 + } + ], + "WAWebDebugLabyrinthInboxSnapshotQuery.graphql": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [], + "end": 368199, + "factorySha256": "eade6aed56abfc3d6f82f226d1d7fad2d1d9604b50547af15336005d1212daa3", + "moduleSha256": "46617125ee497225713367bbeeb7dc48af53054378dfe9b09b7daf961aff5d85", + "start": 365555 + } + ], + "WAWebDebugLabyrinthRangeQuery": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [ + "WAWebDebugLabyrinthRangeQuery.graphql", + "WAWebRelayClient", + "asyncToGeneratorRuntime" + ], + "end": 372987, + "factorySha256": "ad31dde9ba5ab313472c90fe67e4c303e0f56ff8e49991406a04f6751e82ef9d", + "moduleSha256": "e59196f8061a0bcb92ef87e2435a35ea5de3680dddbd41cde86fbee1fc8d64a9", + "start": 371829 + } + ], + "WAWebDebugLabyrinthRangeQuery.graphql": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [], + "end": 371827, + "factorySha256": "42dd91382ad68df23254a0f1090cbee2656a8ccddb0c51b92ff47e1a324f67cc", + "moduleSha256": "229ad749ff907bb05a246b2435c92cfb71e3409d518eea00f52d3500e477f93e", + "start": 369272 + } + ], + "WAWebFilterLogging": [ + { + "bundleSha256": "15339e541ca850185a14c0603d262eed300ad5019b65d3582aaf3a8eeab1cc96", + "dependencies": [ + "WAWebChatFilterEventWamEvent", + "WAWebChatSearchFilters", + "WAWebChatThreadLogging", + "WAWebConnModel", + "WAWebInboxFiltersGatingUtils", + "WAWebLabelCollection", + "WAWebListUtils", + "WAWebListsGatingUtils", + "WAWebListsLabelGatingUtils", + "WAWebListsLogging", + "WAWebWamEnumChatFilterActionTypes", + "WAWebWamEnumChatFilterTargetScreen", + "WAWebWamEnumChatFilterTypes", + "WAWebWamEnumChatSearchResultType", + "asyncToGeneratorRuntime" + ], + "end": 508302, + "factorySha256": "249f8462a2ca5cfe294d73249b76f0797036c591552f6152d678056a56a4178d", + "moduleSha256": "0da3d9c081cbf3b3372af10aa04da2f768a58c316caf63676456e2104fbe7783", + "start": 500906 + } + ], "WAWebKmpSyncdRequestBuilder": [ { "bundleSha256": "ffe69f61924eb23c1d65cb89a152e4ce35ee404bd52a7edcced9c037b236f490", @@ -62,6 +214,108 @@ "start": 3406431 } ], + "WAWebMexLogging": [ + { + "bundleSha256": "657a32007737e50b0cb0292ce9c364e21e48291b558129753b25e3ec2a4c13b7", + "dependencies": [ + "WAWebMexEventV2WamEvent" + ], + "end": 2400008, + "factorySha256": "dd1551ae839013f1bdc42de069d375d5d02cbad9834cdd8f558e1f6ae980c120", + "moduleSha256": "e6d862350fb96e5beb62395ed2f4742252b959cc7970a342b66b24c84e99b621", + "start": 2398464 + } + ], + "WAWebPaymentsUserActionLogging": [ + { + "bundleSha256": "c54c33dd6f74be053881c3a7d0e750f8b8fa04140579821b214ab6f59711bbe0", + "dependencies": [ + "WALogger", + "WAWebPaymentsUserActionWamEvent", + "WAWebWamEnumPaymentActionTypes" + ], + "end": 466809, + "factorySha256": "6390f2e084a8e7ba74eaf207765071c28c0de57f0b74b82ab763199e5480f11a", + "moduleSha256": "725c3fec4cba9a60a82dcb9c2a433194b770beb121fe235bee5bd3a5d3b8d70f", + "start": 465792 + } + ], + "WAWebPixWamLogger": [ + { + "bundleSha256": "cf6197f616a43cf9004101d911d5643e8fa63972b2d27a54e8bba8db062abb8f", + "dependencies": [ + "WAWebGetMessageChatTypeFromWid", + "WAWebPaymentsUserActionWamEvent", + "WAWebSyncdMdSyncFieldstatMeta", + "WAWebWamEnumMessageChatType", + "asyncToGeneratorRuntime" + ], + "end": 1035693, + "factorySha256": "80b3b468f6458f98af9de51f21f47334fcc08d690cf65ce7ccf88859fa01a2a9", + "moduleSha256": "eb24e5bee24de5260c8b6bb504fdf8cf163d20a763f19fd6acf808d805966ad3", + "start": 1034691 + } + ], + "WAWebRotateLabyrinthEpochJob": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [ + "WABase64", + "WAWebLabyrinthDebugStateCache", + "WAWebRelayClient", + "WAWebRotateLabyrinthEpochJobMutation.graphql", + "asyncToGeneratorRuntime", + "err", + "nullthrows" + ], + "end": 395706, + "factorySha256": "6a702a9ce2e3145e3c7b325c2b8679b5297f7a55a5c618a5058b0b9c6c75ef32", + "moduleSha256": "7c2164a4a88836cb0d7380e69903d8a16a069183c02ecf0b2d2af7d0b6430fdd", + "start": 393460 + } + ], + "WAWebRotateLabyrinthEpochJobMutation.graphql": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [], + "end": 393458, + "factorySha256": "82d153f3f2c7dd09163030da23b74ca363b6f0c297ea625fdddf7d6226c5c3d4", + "moduleSha256": "f719eb59511db0b13112d2d165ae99e4c211b935a4a7dccf4683b09afb3db0af", + "start": 392197 + } + ], + "WAWebSendStickerAction": [ + { + "bundleSha256": "15339e541ca850185a14c0603d262eed300ad5019b65d3582aaf3a8eeab1cc96", + "dependencies": [ + "fbt", + "WALogger", + "WAWebAuraGating", + "WAWebBizBotTosUtils", + "WAWebCmd", + "WAWebConfirmPopup.react", + "WAWebMedia", + "WAWebMediaOpaqueData", + "WAWebModalManager", + "WAWebRecentStickerCollection", + "WAWebSendMsgResultAction", + "WAWebStateUtils", + "WAWebStickerPremiumStatus", + "WAWebStickerSendWamEvent", + "WAWebWaPlusBenefitJourneyLogger", + "WAWebWamEnumWpbujBenefitType", + "WAWebWamEnumWpbujSurface", + "WAWebWebpMetadata", + "asyncToGeneratorRuntime", + "err", + "react" + ], + "end": 802363, + "factorySha256": "7b19deb775fb77bc34310f7e4096c7525807a81b62e7bd7b092966f3687b7103", + "moduleSha256": "52d2433b889bdfc527fcf4923a500be1b30648d6a7ffdc68f2f639179bfdfdeb", + "start": 799214 + } + ], "WAWebSetAboutJob": [ { "bundleSha256": "82f783508805f734f16747c4bd0670a496f13004e8a0acd11fe5f45efaffd754", @@ -101,94 +355,1328 @@ "moduleSha256": "01098f3de86fbfcf023a317a096dae1c05b0794b92b4488bde1421dd10f0c72e", "start": 1087286 } - ] - }, - "setHash": "99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00", - "sourceIndexVersion": 1, - "waVersion": "2.3000.1045368834" - }, - "new": { - "bundleCount": 579, - "coverage": "recovered-definitions-only", - "limits": [ - "Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", - "The existing AST extractor does not expose parse diagnostics. Absence from this index is not proof of absence upstream.", - "All recovered occurrences are retained. Equal module names with different hashes need review.", - "Module provenance locates code; field-level semantics may require following dependencies." - ], - "modules": { - "WAWebContactManagerCustomerProfilesQuery": [ + ], + "WAWebSmb3pdAggregatedConversionSignalAction": [ { - "bundleSha256": "36dc1290c51b02500ad640ba5aa793dd1b5e9172afcb71ff327d64cd1f279717", + "bundleSha256": "ffe69f61924eb23c1d65cb89a152e4ce35ee404bd52a7edcced9c037b236f490", "dependencies": [ "WAJids", "WALogger", - "WAWebContactManagerCustomerProfileDecoders", - "WAWebContactManagerCustomerProfilesQuery.graphql", - "WAWebFBLogger", + "WAWebCTWADataSharingModel", + "WAWebCTWAGatingUtils", + "WAWebChatThreadLogging", + "WAWebCommonCTWADataSharing", + "WAWebConversionTupleCollection", + "WAWebCtwa3pdAggregatedConversionWamEvent", + "WAWebDataSharing3pdLidCollection", + "WAWebLidMigrationUtils", + "WAWebWamEnumCtwaDirectionFrom", + "getErrorSafe" + ], + "end": 4132971, + "factorySha256": "79c02239cad1dc25ffec0f9489cda49b2464e1ba55b073beeca29b15e58fb12c", + "moduleSha256": "5ac74826aadb1c4e9aa1d9b02f5bdbc514eb95ff7b5104d38d29d0fefc3f9725", + "start": 4130648 + } + ], + "WAWebStatusSubtitle.react": [ + { + "bundleSha256": "72622fd528bcdb8719660018f754e37ce201276a4c5fdead04f82bc81648de8d", + "dependencies": [ + "fbt", + "invariant", + "WAWebClock", + "WAWebContactCollection", + "WAWebContactGetters", + "WAWebExternalLink.react", + "WAWebFlex.react", + "WAWebFrontendContactGetters", + "WAWebMentionsRefreshedIcon.react", + "WAWebMessageAIContentLabel.react", + "WAWebMsgGetters", + "WAWebMusicGatingUtils", + "WAWebMusicStatusAttribution.react", + "WAWebNewsletterAiContentInfoModalOpener", + "WAWebNewsletterAiContentInfoModalTypes", + "WAWebNewsletterGatingUtils", + "WAWebNewsletterMetadataCollection", + "WAWebProtobufsStatusAttributions.pb", + "WAWebStatusCloseFriendsDialog.react", + "WAWebStatusExternalShareUtils", + "WAWebStatusGatingUtils", + "WAWebStatusNavigateToNewsletterUtils", + "WAWebStatusSubtitleAttribution.react", + "WAWebStatusViewerActionWamEvent", + "WAWebStopEvent", + "WAWebWamEnumStatusCategory", + "WAWebWamEnumStatusViewActionType", + "WAWebWamEnumTopBarAttributionType", + "WDSIconIcRepeat.react", + "WDSIconWdsIcAiContent.react", + "WDSIconWdsIcChannels.react", + "WDSText.react", + "react", + "react-compiler-runtime", + "stylex", + "useWAWebMsgValues" + ], + "end": 518491, + "factorySha256": "0d44a513654a0050ecf1b431758d62cb98f091f4a3598b5e5adb4c8f6e2b481e", + "moduleSha256": "428ad864810309ceae9c7ee254f3422d49a4c60f8320de4d909f4fa7d9704afe", + "start": 506432 + } + ], + "WAWebStatusViewerActionWamEvent": [ + { + "bundleSha256": "72622fd528bcdb8719660018f754e37ce201276a4c5fdead04f82bc81648de8d", + "dependencies": [ + "WAWebWamCodegenUtils", + "WAWebWamEnumInlineVideoType", + "WAWebWamEnumStatusCategory", + "WAWebWamEnumStatusViewActionType", + "WAWebWamEnumTopBarAttributionType", + "WAWebWamEnumUrlStatusClicked", + "WAWebWamEnumUrlStatusType" + ], + "end": 506430, + "factorySha256": "d245955e77af9f95036f6b129de3ccaf8cb09f079c228a9304c9c4824059e715", + "moduleSha256": "188a58b5832e3127ecc99ebc44cb72053645624f24f79df43fa2a37f9d570f33", + "start": 505398 + } + ], + "WAWebTeamLinkCreateInvitationDialog.react": [ + { + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", + "dependencies": [ + "fbt", + "WAWebConfirmPopup.react", + "WAWebContactCollection", + "WAWebContactCollectionUtils", + "WAWebContactImage.react", + "WAWebFrontendContactGetters", + "WAWebL10NAccentFold", + "WAWebLidMigrationUtils", + "WAWebModalManager", + "WAWebPhoneNumberSearch", + "WAWebTeamLinkCreateInvitationMutation", + "WAWebTeamLinkInviteCodeCreatedPopup.react", + "WAWebWidFactory", + "WDSText.react", + "WDSTextField.react", + "react", + "react-compiler-runtime" + ], + "end": 10237, + "factorySha256": "5eea5298ec9001caf66dfa7834691c703283358f11dc9d753f800c41e0b02905", + "moduleSha256": "7281274e4366cfdb5e60a85a61fea908df39a8847f69b0abe71f4b128a80772f", + "start": 4089 + } + ], + "WAWebTeamLinkCreateInvitationMutation": [ + { + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", + "dependencies": [ "WAWebFetchAdAccountToken", - "WAWebGraphQLServerError", - "WAWebNetworkStatus", "WAWebRelayClient", + "WAWebTeamLinkCreateInvitationMutation.graphql", "asyncToGeneratorRuntime", - "err", - "getErrorSafe" + "err" ], - "end": 257662, - "factorySha256": "eb5e7cf190d44a5cbf268623c952ee43e04e348a88b627b196d37d83a31dab66", - "moduleSha256": "eea21df5ba17114fbce760a1d44502f48d0a7d4ba66a4dfbad4e6ce00f8f92ca", - "start": 254574 + "end": 2779, + "factorySha256": "8e937dc92d2d1b76729f3e689b39e9df0e1e6e123547777171ace924643d3e59", + "moduleSha256": "18f6204edb5afad329c1c879b24b407434ab71be335e7b0893977c917ae80186", + "start": 1617 } ], - "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation": [ + "WAWebTeamLinkCreateInvitationMutation.graphql": [ { - "bundleSha256": "36dc1290c51b02500ad640ba5aa793dd1b5e9172afcb71ff327d64cd1f279717", + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", + "dependencies": [ + "WAWebTeamLinkCreateInvitationMutation_facebookRelayOperation" + ], + "end": 1615, + "factorySha256": "59594b36bbb46a05ad7e3eda7d51749b5915d52dbf7bcf441730a811336271d7", + "moduleSha256": "a7e4aed907bb7d586f12a9fcaa9c357da3b81c2067d2a59244b1659e85adb091", + "start": 151 + } + ], + "WAWebTeamLinkCreateInvitationMutation_facebookRelayOperation": [ + { + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", "dependencies": [], - "end": 252770, - "factorySha256": "8ad8f73b1a267fb2b3e43ac1c801326dd5a667f104e3a45fb33b81b01bc99b4c", - "moduleSha256": "0edd9a4622782763c39f80764decf75406cbec76925a80622cfd1ee90436ec81", - "start": 252637 + "end": 149, + "factorySha256": "abd4f7dfbdb7d6c391eedec77854ea013c40cd3236ac7bffea946e5bc355220d", + "moduleSha256": "5af299a346f9c4cf3def3de8964dca759359812438f0f0569dd9d2af718a2c29", + "start": 19 } ], - "WAWebSetAboutJob": [ + "WAWebTeamLinkListInvitationsQuery": [ { - "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", "dependencies": [ - "WADeprecatedSendIq", - "WADeprecatedWapParser", - "WAWap", - "WAWebDefinePersistedJob", - "asyncToGeneratorRuntime" + "WAWebFetchAdAccountToken", + "WAWebRelayClient", + "WAWebTeamLinkListInvitationsQuery.graphql", + "asyncToGeneratorRuntime", + "err" ], - "end": 2111895, - "factorySha256": "5fe82a38fe4610426641d5c0351746693a33b095254e8a358c1b1a3df7a326ef", - "moduleSha256": "6b2d764d05842a76912d40ce70154f6ec5a69f67de25345a2d86d8cfa8c32db0", - "start": 2111070 + "end": 12655, + "factorySha256": "935ce451a8e958ee8d009cf899db54d572d9cc54371de82cfb52492a009f984e", + "moduleSha256": "82fbeffbc316956a49a0065d6f9111353207d471ff17ffee5df86768a48e4379", + "start": 11567 } ], - "WAWebSetPrivacyJob": [ + "WAWebTeamLinkListInvitationsQuery.graphql": [ { - "bundleSha256": "7c7be0378ce2e7ff49b9f5909cf8a7d283000af82cc43bc071c2df3541ec5d56", + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", + "dependencies": [ + "WAWebTeamLinkListInvitationsQuery_facebookRelayOperation" + ], + "end": 11565, + "factorySha256": "d2618679c90ff9cdc2ddb82d8bd69b7a68ee5802a11e4f0feec61cf68c9d16c0", + "moduleSha256": "322b3fcbde0f4c49e8e01048e26cc084b73bd2a2841ec31fcbdc4f43bae91a3d", + "start": 10367 + } + ], + "WAWebTeamLinkListInvitationsQuery_facebookRelayOperation": [ + { + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", + "dependencies": [], + "end": 10365, + "factorySha256": "1df804bf6a1ec43cc76a8de9b4845dd27391fd497cad828fff71bf7924206be1", + "moduleSha256": "a066fa858246be94eab414de5f8661f7ca4f47a2f5f1170cc84e4ac8fb4c20c0", + "start": 10239 + } + ], + "WAWebTeamLinkRemoveInvitationMutation": [ + { + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", + "dependencies": [ + "WAWebFetchAdAccountToken", + "WAWebRelayClient", + "WAWebTeamLinkRemoveInvitationMutation.graphql", + "asyncToGeneratorRuntime", + "err" + ], + "end": 20375, + "factorySha256": "8cb7fd4ba6695f303fadb49946ce8fe4cff951be6372562958f4cb08c287b7ad", + "moduleSha256": "b78124d5eb5224d2fd8478cb2ccd11a94aa94dc4c973b10fbdf6f87cfee7ab18", + "start": 19327 + } + ], + "WAWebTeamLinkRemoveInvitationMutation.graphql": [ + { + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", + "dependencies": [ + "WAWebTeamLinkRemoveInvitationMutation_facebookRelayOperation" + ], + "end": 19325, + "factorySha256": "7976576fde7ade5188ce74ce895448628465b1e0774c61bea0f0ea602b001176", + "moduleSha256": "45c67020cda6937d7978cdb4ee84234afb4aed28755523b966fe3d775edaf85c", + "start": 18231 + } + ], + "WAWebTeamLinkRemoveInvitationMutation_facebookRelayOperation": [ + { + "bundleSha256": "e54c895e9c58163bae88d47ab5017e168c61127955be4307e2b94bacfd42c33e", + "dependencies": [], + "end": 18229, + "factorySha256": "2f9464be4e6def94893ab3e2da3f2cdcfb20a3491a58d93de4e6a99206d446a9", + "moduleSha256": "f851efa4639b0531263a686395e0037e9b6a1275adbbdce276b47b73d91f7bee", + "start": 18099 + } + ], + "WAWebThreadLoggingAi": [ + { + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", "dependencies": [ - "$InternalEnum", - "WADeprecatedSendIq", - "WADeprecatedWapParser", "WALogger", - "WAWap", - "WAWebABProps", - "WAWebBackendErrors", - "WAWebCommsWapMd", - "WAWebLidMigrationUtils", - "WAWebQueryPrivacyDisallowedListUtil", - "WAWebUsernameTypes", + "WAWebThreadInteractionDataAiWamEvent", + "getErrorSafe" + ], + "end": 573839, + "factorySha256": "9c3eabd9a69e12c6cbacbe70f1bd294d486a0d5589aa3f245a50e5e97378686f", + "moduleSha256": "b145e75b68163c093af2109db48aab201cb09b58bf6117e2903e424b33d42720", + "start": 573189 + } + ], + "WAWebThreadLoggingBiz": [ + { + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataBizWamEvent", + "WAWebWamEnumBizCatalogType", + "getErrorSafe" + ], + "end": 577681, + "factorySha256": "278b70e431b064564cba7a0a7794c5884cb010d996599e56f9d04f381e9c84b9", + "moduleSha256": "c131f7f32bcb77192abfc0542045221c12f75e2da33c2594063d999105050d65", + "start": 576593 + } + ], + "WAWebThreadLoggingCoreConsumer": [ + { + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "dependencies": [ + "WALogger", + "WAWebGroupType", + "WAWebThreadInteractionDataCoreConsumerWamEvent", + "WAWebWamEnumChatMutedType", + "err" + ], + "end": 588569, + "factorySha256": "efd62b70dffcb0ab65c0bb54da77ab8f8df2abd25b734905e29103d1578cb750", + "moduleSha256": "5a69f324b23edd70ceaa5fe6c1252f56a493b61fadb5f55f5764dd16c2d0661e", + "start": 585177 + } + ], + "WAWebThreadLoggingIntegrity": [ + { + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataIntegrityWamEvent", + "getErrorSafe" + ], + "end": 590052, + "factorySha256": "65ebd51ffac910e7ae9952860bc99e703409664faba802618e9e9fb731615353", + "moduleSha256": "35589ea4523873fe91790e9bd6f73589e1886b53eb5fc6f8f1a391d9d5b69531", + "start": 589451 + } + ], + "WAWebThreadLoggingNotification": [ + { + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataNotificationWamEvent", + "err" + ], + "end": 592605, + "factorySha256": "4565b9c412a5677abbd9e6169dca262bc240495948ded7d4fe580e1a2524efb0", + "moduleSha256": "33b01e6054497198f4b082a7a7b63724d26ae37eb0cbdf16941ebf46a52fc5d9", + "start": 591926 + } + ], + "WAWebThreadLoggingVoip": [ + { + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataVoipWamEvent", + "getErrorSafe" + ], + "end": 594125, + "factorySha256": "3657b2b71a5b716b563df84a1c513ef66980b2dfba7720e90dc462e3ab2796ab", + "moduleSha256": "524103e748da6c43badf73f0062a2b873b1042822a04f95671e6a325f716fdef", + "start": 593439 + } + ], + "WAWebUploadLabyrinthMessagesJob": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [ + "WABase64", + "WAWebRelayClient", + "WAWebUploadLabyrinthMessagesJobMutation.graphql", "asyncToGeneratorRuntime", "err" ], - "end": 186813, - "factorySha256": "f2e16b840638226fe6023fe91bb0bbf3bcae8b5bc31967504cba8db759748abb", - "moduleSha256": "270ab2fc00118aae4e79742cd84606290ed14b06d80a5c83da46cdbe2573cb60", + "end": 398367, + "factorySha256": "bf42741795ea2a9583ce8be900980b60a28befa7042c8c749de5573f9b881807", + "moduleSha256": "dc39af19f07da1705a6873b1726456a49338a3a08640f99702cf736ab92771e7", + "start": 397286 + } + ], + "WAWebUploadLabyrinthMessagesJobMutation.graphql": [ + { + "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", + "dependencies": [], + "end": 397284, + "factorySha256": "9da7f56c55de785d4c730524225922f14e03d3c8f166fb9e226df9dea4d111b1", + "moduleSha256": "c15c44b1f548427cd50a73cb1a3a5e640df7f48749eb84a4b84b24bebc9037b6", + "start": 395708 + } + ], + "WAWebWamCodegenUtils": [ + { + "bundleSha256": "657a32007737e50b0cb0292ce9c364e21e48291b558129753b25e3ec2a4c13b7", + "dependencies": [ + "WAUpperFirst", + "WAWebWamCodegenGlobalMetrics", + "WAWebWamCodegenWamEvent", + "WAWebWamInitQueue", + "WAWebWamRuntimeProvider", + "WAWebWamTypeHash", + "err", + "gkx", + "justknobx" + ], + "end": 483531, + "factorySha256": "e4f82a8795da9d43859240c6347b2ce6036970879583b89f916c0dc1f16818d6", + "moduleSha256": "13572093631b91cfe8508e3915a5d7fcecad45ce8e7034a1f6987328649afdef", + "start": 480857 + } + ], + "WAWebWamFingerprintReporter": [ + { + "bundleSha256": "ffe69f61924eb23c1d65cb89a152e4ce35ee404bd52a7edcced9c037b236f490", + "dependencies": [ + "WAWebWamEnumWebcWindowNavigatorWebdriverType", + "WAWebWamFingerprint", + "WAWebWebcFingerprintWamEvent", + "asyncToGeneratorRuntime" + ], + "end": 3447511, + "factorySha256": "1c5038f4828e0759bdc0e9b62823db74cb4cdb034babbf5ae734dd9f191a096a", + "moduleSha256": "3a2452fcfc9277edf7761595c092341f3ca70594087fbce930759a9322854972", + "start": 3445837 + } + ], + "WAWebWamTypeHash": [ + { + "bundleSha256": "657a32007737e50b0cb0292ce9c364e21e48291b558129753b25e3ec2a4c13b7", + "dependencies": [ + "WALogger", + "err" + ], + "end": 480855, + "factorySha256": "f5f79ada80106f01ad2c999bea947d73a295a5dbbaaeae183f2bada9f6a3df13", + "moduleSha256": "08a201d682b60cf3752df260fb05082ae2c53f880e92d60c43edfa1f7cba9950", + "start": 479639 + } + ], + "WAWebWindowsHybridBridgeWam.v2589": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType" + ], + "end": 125810, + "factorySha256": "a6a7aa7dd51c75a0ca5348b4cc3a7c2c6604bb9a7f58889284a77e23574d7133", + "moduleSha256": "9b5e5d06a2837493e923b8e453d6417d026a573e415818bc716ceb5b6d34821a", + "start": 124490 + } + ], + "WAWebWindowsHybridBridgeWam.v2590": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 172663, + "factorySha256": "de4acebdb426e2952fcb73c9a80f892fef7f597ce574377f245cf73da89e447a", + "moduleSha256": "6ac3c32ead0dfc6461308cbf8cea452fd14b529ad72755868540203cdfc4da7f", + "start": 170166 + } + ], + "WAWebWindowsHybridBridgeWam.v2591": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 219516, + "factorySha256": "3a5a25bdff6973005434414e38da36dfc50169de75c61bc82e69c9c055615d64", + "moduleSha256": "5f540bb3ff085068942ad88ee3efd3a4b65a1b527a6afcd27582533e8067086a", + "start": 217019 + } + ], + "WAWebWindowsHybridBridgeWam.v2601": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 266418, + "factorySha256": "b4f51fae858722f0183670d0c6797bc2ac4937504bce97512bfea72eb83f91d2", + "moduleSha256": "03e94c2e2ea6a19c474fa4dee9df7d8b9f3f980cf10d4858d83a160706376992", + "start": 263921 + } + ], + "WAWebWindowsHybridBridgeWam.v2602": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 313347, + "factorySha256": "a92b4faffe000d62de79c5c05616e006455fa058f2c34f86409df12c770eb570", + "moduleSha256": "e75831087104eae8d2c4726e1cfbfa3d309c5fba6f65c4e40b0b6b562f794ff0", + "start": 310850 + } + ], + "WAWebWindowsHybridBridgeWam.v2603": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 360646, + "factorySha256": "34c449aab2de6d34dbf6b4d9321bce61734c9a52046ee39363d13d57165c814f", + "moduleSha256": "d77a760a864285f46c383b024cbde7b5c26394e1c3ec572468e31a15d17c9bc4", + "start": 358149 + } + ], + "WAWebWindowsHybridBridgeWam.v2604": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 408054, + "factorySha256": "250e0de98dfa10624f9182c6d4a2e29b797c0e84b983ad5c9ef25c4006278624", + "moduleSha256": "e9b67ca63d540329c07058ff9f4b0581978f5baad499bda18a45a83bfb1835c9", + "start": 405413 + } + ], + "WAWebWindowsHybridBridgeWam.v2608": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 598024, + "factorySha256": "b0537016ddbf3b5386d6c6fc2779f11617387c2661e2513a1fd4a542737fe1e7", + "moduleSha256": "8129f2fbd90380f5b4969f9b04899e219fd44d409139bece8e6059d9a2e07cdf", + "start": 595524 + } + ], + "WAWebWindowsHybridBridgeWam.v2609": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 645376, + "factorySha256": "d4505ec737e7de3d8289c59536458012e62b46ef4c592214521a606caef33f5e", + "moduleSha256": "a9a43e5fd22acfd7bb2c73ab3d8f3487886bf7d35694b80881f226945409615f", + "start": 643020 + } + ], + "WAWebWindowsHybridBridgeWam.v2610": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 692997, + "factorySha256": "4f50b69db9c9c7fd7312670ae15ec53d18e078e3e556c3237b2870b120f67d38", + "moduleSha256": "be912cbd9fb40818cc7bd9edc0149ec4d4011cd54f94af28d9dd4b88c5db9b9d", + "start": 690641 + } + ], + "WAWebWindowsHybridBridgeWam.v2617": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 1061781, + "factorySha256": "97f87c776396abd08f01945283a798a69579fcc010c405f34e5699b737b2b7a5", + "moduleSha256": "f3a3b5c0778f2d6b2a119bcad4daa6fd8a7d25ae89a843c524bf316cc53cfaf7", + "start": 1059425 + } + ], + "WAWebWindowsHybridBridgeWam.v2618": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebBrowserApi", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 1136656, + "factorySha256": "4656aac6a4172aa1f646936abf5ee4b913b234187abd9689919b06f10a69784b", + "moduleSha256": "35fce397454c009eeda1091553a59b591f551525e93ea28e9d903be86ffc170d", + "start": 1134200 + } + ], + "WAWebWindowsHybridBridgeWam.v2619": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebAppTracker", + "WAWebBrowserApi", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebMemoryStatWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 1211705, + "factorySha256": "e3ad5c503bc234c7d94b518e9826e1df4b1112323fc6999da2855497b3b743b9", + "moduleSha256": "52149f3f19ecae9222b4586e45865ac36505c55a2702a322355445cb858ca41f", + "start": 1209075 + } + ], + "WAWebWindowsHybridBridgeWam.v2621": [ + { + "bundleSha256": "3a7550f6d04d8fb6a924282ad5e37e0e0618ec8934f6be01932628030722ceda", + "dependencies": [ + "Promise", + "WALogger", + "WAWebAppTracker", + "WAWebBrowserApi", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebMemoryStatWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 1363649, + "factorySha256": "554b7b227b6248256978fa73978483cc19e2a6badeb61b40b98d72dbbfcbab2f", + "moduleSha256": "f8b9813fee8dd5448aa5130393d15c4c1ecbd23530c914cdbae2bf52f3265482", + "start": 1361019 + } + ], + "WAWebWindowsHybridBridgeWam.v2624": [ + { + "bundleSha256": "b0cfc2897a22e9dcdb36b15743b3ea87b925b868559df1477b8d4cbca10750d8", + "dependencies": [ + "Promise", + "WALogger", + "WAWebAppTracker", + "WAWebBrowserApi", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebMemoryStatWamEvent", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "asyncToGeneratorRuntime" + ], + "end": 233045, + "factorySha256": "55145374fd2606d5249043ad1de8f936a5e60e7faca8e4c878d662e271fbbebf", + "moduleSha256": "035db025583592f7fe9fe4b7ad2b89f52f962b36ffb636e521666195e6379b4c", + "start": 230415 + } + ] + }, + "setHash": "99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00", + "sourceIndexVersion": 1, + "waVersion": "2.3000.1045368834" + }, + "new": { + "bundleCount": 579, + "coverage": "parsed-static-definitions", + "limits": [ + "Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", + "Every bundle parsed without recovery. Absence only establishes that this exact static module name was not defined in these inputs; it does not prove feature removal or a rename.", + "All recovered occurrences are retained. Equal module names with different hashes need review.", + "Module provenance locates code; field-level semantics may require following dependencies." + ], + "modules": { + "WAWebAccountLinkingAPIGetCertsQuery.graphql": [ + { + "bundleSha256": "f588ee2f033b87e018a3be34a37ec1347d8b6b8700de539e6be31481b29c4690", + "dependencies": [], + "end": 2182951, + "factorySha256": "71c349b85981857d87de20e0058b3294b338fab20a48077494a25170e27cc175", + "moduleSha256": "a5c80e9f78b2920e445d2f444845bcd25662352dd617a02279311db7c9f3de06", + "start": 2181768 + } + ], + "WAWebContactManagerCustomerProfilesQuery": [ + { + "bundleSha256": "36dc1290c51b02500ad640ba5aa793dd1b5e9172afcb71ff327d64cd1f279717", + "dependencies": [ + "WAJids", + "WALogger", + "WAWebContactManagerCustomerProfileDecoders", + "WAWebContactManagerCustomerProfilesQuery.graphql", + "WAWebFBLogger", + "WAWebFetchAdAccountToken", + "WAWebGraphQLServerError", + "WAWebNetworkStatus", + "WAWebRelayClient", + "asyncToGeneratorRuntime", + "err", + "getErrorSafe" + ], + "end": 257662, + "factorySha256": "eb5e7cf190d44a5cbf268623c952ee43e04e348a88b627b196d37d83a31dab66", + "moduleSha256": "eea21df5ba17114fbce760a1d44502f48d0a7d4ba66a4dfbad4e6ce00f8f92ca", + "start": 254574 + } + ], + "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation": [ + { + "bundleSha256": "36dc1290c51b02500ad640ba5aa793dd1b5e9172afcb71ff327d64cd1f279717", + "dependencies": [], + "end": 252770, + "factorySha256": "8ad8f73b1a267fb2b3e43ac1c801326dd5a667f104e3a45fb33b81b01bc99b4c", + "moduleSha256": "0edd9a4622782763c39f80764decf75406cbec76925a80622cfd1ee90436ec81", + "start": 252637 + } + ], + "WAWebFilterLogging": [ + { + "bundleSha256": "3759ae9b91f03d11301433abb539dbe1cf575b9ba83d71cac768aa5fd662f76a", + "dependencies": [ + "WAWebChatFilterEventWamEvent", + "WAWebChatSearchFilters", + "WAWebChatThreadLogging", + "WAWebConnGetters", + "WAWebConnModel", + "WAWebInboxFiltersGatingUtils", + "WAWebLabelCollection", + "WAWebListUtils", + "WAWebListsGatingUtils", + "WAWebListsLabelGatingUtils", + "WAWebListsLogging", + "WAWebWamEnumChatFilterActionTypes", + "WAWebWamEnumChatFilterTargetScreen", + "WAWebWamEnumChatFilterTypes", + "WAWebWamEnumChatSearchResultType", + "asyncToGeneratorRuntime" + ], + "end": 407194, + "factorySha256": "a0db83c75fe040009567e94d701af42f964b21895a39d0caed0ed896ef252e65", + "moduleSha256": "85cbd606e0574186e121e596c1470e1f97ec3afe3fee67bdce69d9e309082669", + "start": 399753 + } + ], + "WAWebMexLogging": [ + { + "bundleSha256": "430cb8979511aada95178605faa35a8483446f677ce6d1fe0fd66333c7879eee", + "dependencies": [ + "WAWebMexEventV2WamEvent" + ], + "end": 1052144, + "factorySha256": "dd1551ae839013f1bdc42de069d375d5d02cbad9834cdd8f558e1f6ae980c120", + "moduleSha256": "e6d862350fb96e5beb62395ed2f4742252b959cc7970a342b66b24c84e99b621", + "start": 1050600 + } + ], + "WAWebMexUpdateNewsletterAdminProfileSettingJob": [ + { + "bundleSha256": "8b1ad690213935d2ecab20746541b09231161494bd7be0a24dd95c6eb2f59366", + "dependencies": [ + "WAWebBackendErrors", + "WAWebMexClient", + "WAWebMexUpdateNewsletterAdminProfileSettingJobMutation.graphql", + "asyncToGeneratorRuntime" + ], + "end": 488568, + "factorySha256": "302bfa3bea962255b9ad0f22eb78f451aab7f28423aadc633b89a1a373c4e85a", + "moduleSha256": "d0394b7ed730757f0fe17c7a5b1c17962ce8302098c6a8767e8a69f66c2abb1c", + "start": 487492 + } + ], + "WAWebMexUpdateNewsletterAdminProfileSettingJobMutation.graphql": [ + { + "bundleSha256": "8b1ad690213935d2ecab20746541b09231161494bd7be0a24dd95c6eb2f59366", + "dependencies": [], + "end": 487490, + "factorySha256": "f0413602fff7b0686d4735f017f6041bfc6665a606e3b6527a7c7d2e3a25bd8d", + "moduleSha256": "a67f3672a91e2d1b66e3015540b6c402c91734ce4d614e361ee46fce41699ac8", + "start": 486221 + } + ], + "WAWebPaymentsUserActionLogging": [ + { + "bundleSha256": "b6a60494b96941d4f130f165112647667426337cb4060c63cfeb38330fb38bf1", + "dependencies": [ + "WALogger", + "WAWebPaymentsUserActionWamEvent", + "WAWebWamEnumPaymentActionTypes" + ], + "end": 25782, + "factorySha256": "6390f2e084a8e7ba74eaf207765071c28c0de57f0b74b82ab763199e5480f11a", + "moduleSha256": "725c3fec4cba9a60a82dcb9c2a433194b770beb121fe235bee5bd3a5d3b8d70f", + "start": 24765 + } + ], + "WAWebPixWamLogger": [ + { + "bundleSha256": "9b681e073f14f9d50e3dc12b456e4f980b554eff4d58827bc91c90a697e9ca38", + "dependencies": [ + "WALogger", + "WAWebGetMessageChatTypeFromWid", + "WAWebPaymentsUserActionWamEvent", + "WAWebSyncdMdSyncFieldstatMeta", + "WAWebWamEnumMessageChatType", + "asyncToGeneratorRuntime" + ], + "end": 5288514, + "factorySha256": "207a8ec627293f16757496b82f22d4ea6c222524b4916a42038c3d9da061cfd9", + "moduleSha256": "85b220417f61747199d1677d0d5af33cd04f8d0d683f2435e5685f9eb536fb1f", + "start": 5286758 + }, + { + "bundleSha256": "ee46bf4858a1118a5a2c15d36c4ddc414fa927c6d527922616e4f2951ca87046", + "dependencies": [ + "WALogger", + "WAWebGetMessageChatTypeFromWid", + "WAWebPaymentsUserActionWamEvent", + "WAWebSyncdMdSyncFieldstatMeta", + "WAWebWamEnumMessageChatType", + "asyncToGeneratorRuntime" + ], + "end": 146645, + "factorySha256": "207a8ec627293f16757496b82f22d4ea6c222524b4916a42038c3d9da061cfd9", + "moduleSha256": "85b220417f61747199d1677d0d5af33cd04f8d0d683f2435e5685f9eb536fb1f", + "start": 144889 + } + ], + "WAWebSendStickerAction": [ + { + "bundleSha256": "3759ae9b91f03d11301433abb539dbe1cf575b9ba83d71cac768aa5fd662f76a", + "dependencies": [ + "fbt", + "WALogger", + "WAWebAuraGating", + "WAWebBizBotTosUtils", + "WAWebCmd", + "WAWebConfirmPopup.react", + "WAWebMedia", + "WAWebMediaOpaqueData", + "WAWebModalManager", + "WAWebRecentStickerCollection", + "WAWebSendMsgResultAction", + "WAWebStateUtils", + "WAWebStickerPremiumStatus", + "WAWebStickerSendWamEvent", + "WAWebWaPlusBenefitJourneyLogger", + "WAWebWamEnumWpbujBenefitType", + "WAWebWamEnumWpbujSurface", + "WAWebWebpMetadata", + "asyncToGeneratorRuntime", + "err", + "react" + ], + "end": 720666, + "factorySha256": "a48bdf72086f3ff767d48724cdde328203128cecbaee171ae348b2bb41bae033", + "moduleSha256": "9357b5727a22496e1630d342ca87220f8288c6e18bf81a98a8f4db05e47db6a5", + "start": 717479 + } + ], + "WAWebSetAboutJob": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WADeprecatedSendIq", + "WADeprecatedWapParser", + "WAWap", + "WAWebDefinePersistedJob", + "asyncToGeneratorRuntime" + ], + "end": 2111895, + "factorySha256": "5fe82a38fe4610426641d5c0351746693a33b095254e8a358c1b1a3df7a326ef", + "moduleSha256": "6b2d764d05842a76912d40ce70154f6ec5a69f67de25345a2d86d8cfa8c32db0", + "start": 2111070 + } + ], + "WAWebSetPrivacyJob": [ + { + "bundleSha256": "7c7be0378ce2e7ff49b9f5909cf8a7d283000af82cc43bc071c2df3541ec5d56", + "dependencies": [ + "$InternalEnum", + "WADeprecatedSendIq", + "WADeprecatedWapParser", + "WALogger", + "WAWap", + "WAWebABProps", + "WAWebBackendErrors", + "WAWebCommsWapMd", + "WAWebLidMigrationUtils", + "WAWebQueryPrivacyDisallowedListUtil", + "WAWebUsernameTypes", + "asyncToGeneratorRuntime", + "err" + ], + "end": 186813, + "factorySha256": "f2e16b840638226fe6023fe91bb0bbf3bcae8b5bc31967504cba8db759748abb", + "moduleSha256": "270ab2fc00118aae4e79742cd84606290ed14b06d80a5c83da46cdbe2573cb60", "start": 183590 } + ], + "WAWebSmb3pdAggregatedConversionSignalAction": [ + { + "bundleSha256": "f588ee2f033b87e018a3be34a37ec1347d8b6b8700de539e6be31481b29c4690", + "dependencies": [ + "WAJids", + "WALogger", + "WAWebCTWADataSharingModel", + "WAWebCTWAGatingUtils", + "WAWebChatThreadLogging", + "WAWebCommonCTWADataSharing", + "WAWebConversionTupleCollection", + "WAWebCtwa3pdAggregatedConversionWamEvent", + "WAWebDataSharing3pdLidCollection", + "WAWebLidMigrationUtils", + "WAWebWamEnumCtwaDirectionFrom", + "getErrorSafe" + ], + "end": 2529906, + "factorySha256": "79c02239cad1dc25ffec0f9489cda49b2464e1ba55b073beeca29b15e58fb12c", + "moduleSha256": "5ac74826aadb1c4e9aa1d9b02f5bdbc514eb95ff7b5104d38d29d0fefc3f9725", + "start": 2527583 + } + ], + "WAWebStatusSubtitle.react": [ + { + "bundleSha256": "7a224a1f4e67acb7e519b9676abfa86806e50426e7ed4a9f76e517536fe279ca", + "dependencies": [ + "fbt", + "invariant", + "WALogger", + "WAWebClock", + "WAWebContactCollection", + "WAWebContactGetters", + "WAWebExternalLink.react", + "WAWebFlex.react", + "WAWebFrontendContactGetters", + "WAWebMentionsRefreshedIcon.react", + "WAWebMessageAIContentLabel.react", + "WAWebMsgGetters", + "WAWebMusicGatingUtils", + "WAWebMusicStatusAttribution.react", + "WAWebNewsletterAiContentInfoModalOpener", + "WAWebNewsletterAiContentInfoModalTypes", + "WAWebNewsletterGatingUtils", + "WAWebNewsletterMetadataCollection", + "WAWebProtobufsStatusAttributions.pb", + "WAWebStatusCloseFriendsDialog.react", + "WAWebStatusExternalShareUtils", + "WAWebStatusGatingUtils", + "WAWebStatusLoggingUtils", + "WAWebStatusNavigateToNewsletterUtils", + "WAWebStatusSubtitleAttribution.react", + "WAWebStatusViewerActionWamEvent", + "WAWebStopEvent", + "WAWebWamEnumStatusCategory", + "WAWebWamEnumStatusViewActionType", + "WAWebWamEnumTopBarAttributionType", + "WDSIconIcRepeat.react", + "WDSIconWdsIcAiContent.react", + "WDSIconWdsIcChannels.react", + "WDSText.react", + "asyncToGeneratorRuntime", + "react", + "react-compiler-runtime", + "stylex", + "useWAWebMsgValues" + ], + "end": 315755, + "factorySha256": "88517991c525308897e980ad22667bb2cda74254644fbf6fa1ab761bddba65bd", + "moduleSha256": "e3bd54e8504c4749609e0d99d9ed4f425065deb6ccc7a98a6a0c1ed24f91a591", + "start": 302954 + } + ], + "WAWebStatusViewerActionWamEvent": [ + { + "bundleSha256": "7a224a1f4e67acb7e519b9676abfa86806e50426e7ed4a9f76e517536fe279ca", + "dependencies": [ + "WAWebWamCodegenUtils", + "WAWebWamEnumInlineVideoType", + "WAWebWamEnumStatusCategory", + "WAWebWamEnumStatusViewActionType", + "WAWebWamEnumTopBarAttributionType", + "WAWebWamEnumUrlStatusClicked", + "WAWebWamEnumUrlStatusType" + ], + "end": 302952, + "factorySha256": "d245955e77af9f95036f6b129de3ccaf8cb09f079c228a9304c9c4824059e715", + "moduleSha256": "188a58b5832e3127ecc99ebc44cb72053645624f24f79df43fa2a37f9d570f33", + "start": 301920 + } + ], + "WAWebThreadLoggingAi": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataAiWamEvent", + "getErrorSafe" + ], + "end": 1550564, + "factorySha256": "9c3eabd9a69e12c6cbacbe70f1bd294d486a0d5589aa3f245a50e5e97378686f", + "moduleSha256": "b145e75b68163c093af2109db48aab201cb09b58bf6117e2903e424b33d42720", + "start": 1549914 + } + ], + "WAWebThreadLoggingBiz": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataBizWamEvent", + "WAWebWamEnumBizCatalogType", + "getErrorSafe" + ], + "end": 1554406, + "factorySha256": "278b70e431b064564cba7a0a7794c5884cb010d996599e56f9d04f381e9c84b9", + "moduleSha256": "c131f7f32bcb77192abfc0542045221c12f75e2da33c2594063d999105050d65", + "start": 1553318 + } + ], + "WAWebThreadLoggingCoreConsumer": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WALogger", + "WAWebGroupType", + "WAWebThreadInteractionDataCoreConsumerWamEvent", + "WAWebWamEnumChatMutedType", + "err" + ], + "end": 1565294, + "factorySha256": "efd62b70dffcb0ab65c0bb54da77ab8f8df2abd25b734905e29103d1578cb750", + "moduleSha256": "5a69f324b23edd70ceaa5fe6c1252f56a493b61fadb5f55f5764dd16c2d0661e", + "start": 1561902 + } + ], + "WAWebThreadLoggingIntegrity": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataIntegrityWamEvent", + "getErrorSafe" + ], + "end": 1566777, + "factorySha256": "65ebd51ffac910e7ae9952860bc99e703409664faba802618e9e9fb731615353", + "moduleSha256": "35589ea4523873fe91790e9bd6f73589e1886b53eb5fc6f8f1a391d9d5b69531", + "start": 1566176 + } + ], + "WAWebThreadLoggingNotification": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataNotificationWamEvent", + "err" + ], + "end": 1569330, + "factorySha256": "4565b9c412a5677abbd9e6169dca262bc240495948ded7d4fe580e1a2524efb0", + "moduleSha256": "33b01e6054497198f4b082a7a7b63724d26ae37eb0cbdf16941ebf46a52fc5d9", + "start": 1568651 + } + ], + "WAWebThreadLoggingVoip": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WALogger", + "WAWebThreadInteractionDataVoipWamEvent", + "getErrorSafe" + ], + "end": 1570850, + "factorySha256": "3657b2b71a5b716b563df84a1c513ef66980b2dfba7720e90dc462e3ab2796ab", + "moduleSha256": "524103e748da6c43badf73f0062a2b873b1042822a04f95671e6a325f716fdef", + "start": 1570164 + } + ], + "WAWebUpdateNewsletterAdminProfileSettingJob": [ + { + "bundleSha256": "8b1ad690213935d2ecab20746541b09231161494bd7be0a24dd95c6eb2f59366", + "dependencies": [ + "WAJobOrchestratorTypes", + "WALogger", + "WAWebDbErrors", + "WAWebMexUpdateNewsletterAdminProfileSettingJob", + "WAWebOrchestratorNonPersistedJob", + "WAWebSchemaNewsletterMetadata", + "asyncToGeneratorRuntime" + ], + "end": 489917, + "factorySha256": "6403088b769e61ff345715cd7ba6a3305e0679479e30dff61f4960bf929c078c", + "moduleSha256": "1c25ec9a3232c9e7fa16b460956a30ffdddb87ce4955891304b55e8395381df9", + "start": 488570 + } + ], + "WAWebWamCodegenUtils": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WAUpperFirst", + "WAWebWamCodegenGlobalMetrics", + "WAWebWamCodegenWamEvent", + "WAWebWamInitQueue", + "WAWebWamRuntimeProvider", + "WAWebWamTypeHash", + "err", + "gkx", + "justknobx" + ], + "end": 2609234, + "factorySha256": "e4f82a8795da9d43859240c6347b2ce6036970879583b89f916c0dc1f16818d6", + "moduleSha256": "13572093631b91cfe8508e3915a5d7fcecad45ce8e7034a1f6987328649afdef", + "start": 2606560 + } + ], + "WAWebWamFingerprintReporter": [ + { + "bundleSha256": "f588ee2f033b87e018a3be34a37ec1347d8b6b8700de539e6be31481b29c4690", + "dependencies": [ + "WAWebWamEnumWebcWindowNavigatorWebdriverType", + "WAWebWamFingerprint", + "WAWebWebcFingerprintWamEvent", + "asyncToGeneratorRuntime" + ], + "end": 1822832, + "factorySha256": "1c5038f4828e0759bdc0e9b62823db74cb4cdb034babbf5ae734dd9f191a096a", + "moduleSha256": "3a2452fcfc9277edf7761595c092341f3ca70594087fbce930759a9322854972", + "start": 1821158 + } + ], + "WAWebWamTypeHash": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WALogger", + "err" + ], + "end": 2606558, + "factorySha256": "f5f79ada80106f01ad2c999bea947d73a295a5dbbaaeae183f2bada9f6a3df13", + "moduleSha256": "08a201d682b60cf3752df260fb05082ae2c53f880e92d60c43edfa1f7cba9950", + "start": 2605342 + } + ], + "WAWebWindowsHybridBridgeWam.v2634": [ + { + "bundleSha256": "0329fc9ac66fae345da0ffbef241d3d4fdc1fff852588f9d79de5418b3ecca21", + "dependencies": [ + "Promise", + "WALogger", + "WAWebAppTracker", + "WAWebBrowserApi", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebMemoryStatWamEvent", + "WAWebODS", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "WAWebWindowsHybridBridgeTrace", + "asyncToGeneratorRuntime", + "getErrorSafe" + ], + "end": 943224, + "factorySha256": "e0b9e1f866d0c4557c43de35a4ca81400920d5dccdeece2ce86a256c56a0cfd3", + "moduleSha256": "f2c58543b972edc6a42d7b20868cd72e5b2683b6cabdafc57d734ad7eac33521", + "start": 939823 + } + ], + "WAWebWindowsHybridBridgeWam.v2635": [ + { + "bundleSha256": "0329fc9ac66fae345da0ffbef241d3d4fdc1fff852588f9d79de5418b3ecca21", + "dependencies": [ + "Promise", + "WALogger", + "WAWebAppTracker", + "WAWebBrowserApi", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebMemoryStatWamEvent", + "WAWebODS", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "WAWebWindowsHybridBridgeTrace", + "asyncToGeneratorRuntime", + "getErrorSafe" + ], + "end": 1025261, + "factorySha256": "9e7b790524142b951280b664b7c9383f23234455d2b292f3e2d7efb8f90258c7", + "moduleSha256": "9ed6cb2d54ed70eb1968edc83c190fcb1c71bf33bb8f568f683e14e631a89606", + "start": 1021860 + } + ], + "WAWebWindowsHybridBridgeWam.v2636": [ + { + "bundleSha256": "0329fc9ac66fae345da0ffbef241d3d4fdc1fff852588f9d79de5418b3ecca21", + "dependencies": [ + "Promise", + "WALogger", + "WAWebAppTracker", + "WAWebBrowserApi", + "WAWebCrashContextUtils", + "WAWebCrashLogWamEvent", + "WAWebMemoryStatWamEvent", + "WAWebODS", + "WAWebWam", + "WAWebWamCodegenUtils", + "WAWebWamCodegenWamEvent", + "WAWebWamEnumCrashApplicationState", + "WAWebWamEnumCrashType", + "WAWebWindowsHybridBridgeTrace", + "asyncToGeneratorRuntime", + "getErrorSafe" + ], + "end": 1107250, + "factorySha256": "bb865b80c42ce5cdc1000b6798463e1ee7e744bbf3af8b4923b4bb9cf416c3a1", + "moduleSha256": "9b50581a4f977098b476f5ea06c3fe5d733284f5eb07aa736fa8fcc8d50faddc", + "start": 1103849 + } + ], + "useWAWebSmartComposerCoachSuggestedReply": [ + { + "bundleSha256": "b7acfd3e4ea27fa022100445c8fa2a601a57145499683fa67d19ab8648f8b15e", + "dependencies": [ + "CometRelay", + "Promise", + "WALogger", + "WAWebBizAIRelayRefreshContext", + "WAWebFetchAdAccountToken", + "WAWebGraphQLServerError", + "WAWebNetworkStatus", + "getErrorSafe", + "react", + "react-compiler-runtime", + "useWAWebSmartComposerCoachSuggestedReplyMutation.graphql" + ], + "end": 14943, + "factorySha256": "d7824073f9e5414b6fdc37cd409e0b4e307d7f76c57fc7fc58d4420e6c7c7ad9", + "moduleSha256": "637db081e0f62ba80db007bb678a08ae5a9a8b842c8cc6e29dd31b6492dba1f6", + "start": 12640 + } + ], + "useWAWebSmartComposerCoachSuggestedReplyMutation.graphql": [ + { + "bundleSha256": "b7acfd3e4ea27fa022100445c8fa2a601a57145499683fa67d19ab8648f8b15e", + "dependencies": [ + "useWAWebSmartComposerCoachSuggestedReplyMutation_facebookRelayOperation" + ], + "end": 12638, + "factorySha256": "51d9dbe5350cbb1f989dfd4fb410f48653fbd046e5e8705045bffee15944a2f8", + "moduleSha256": "922022f08ace6dc002a720f81a7a64151957ab565d08ecda44e2d28968bcedfe", + "start": 11529 + } + ], + "useWAWebSmartComposerCoachSuggestedReplyMutation_facebookRelayOperation": [ + { + "bundleSha256": "b7acfd3e4ea27fa022100445c8fa2a601a57145499683fa67d19ab8648f8b15e", + "dependencies": [], + "end": 11527, + "factorySha256": "8da5272921c775f74b767e6b65eea168c820f523ba047cf1091214ce601195bc", + "moduleSha256": "fc69158c537186402667d65683a2e813f52caf801bc0bfe77428ef39fdf7b192", + "start": 11386 + } ] }, "setHash": "05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b", diff --git a/docs/snapshot-quality.md b/docs/snapshot-quality.md index 5d5270c4..36c26df0 100644 --- a/docs/snapshot-quality.md +++ b/docs/snapshot-quality.md @@ -45,7 +45,7 @@ whatspec source-index generated/bundles.lock.json bundles WAWebSetPrivacyJob > s ``` `source-index` independently checks the exact bundle multiset. It parses modules -with the existing AST extractor and emits a separate optional sidecar containing +with a checked AST entry point and emits a separate optional sidecar containing bundle SHA-256, module and factory hashes, dependency names, and byte offsets. It never evaluates JavaScript. Source names may be selected after the two paths; omit them to index all recovered modules. Offsets are UTF-8 bytes with an @@ -53,13 +53,24 @@ exclusive end, so invalid UTF-8 is rejected rather than decoded lossily. Every recovered occurrence remains visible, including conflicting definitions. The index is deterministic and contains no workspace-specific file paths. -The existing AST API does not expose parse diagnostics. Consequently the sidecar -states `coverage: recovered-definitions-only`: a missing name is not proof of -upstream removal. A module hint in a contract report is also only a starting -point. Follow dependencies and mixins before attributing a field's semantics. +The sidecar requires every locked bundle to parse without recovery and states +`coverage: parsed-static-definitions`. A missing exact name proves only that no +matching static definition was recovered from these fully parsed inputs. It does +not prove feature removal, dynamic-module absence, or a rename. A module hint in +a contract report is also only a starting point. Follow dependencies and mixins before attributing a field's semantics. For a field-level review, cite both its IR pointer and the relevant module span. This keeps recoverable provenance outside every repeated field of the IR. +`whatspec quality-gaps LOCK BUNDLE_DIR` emits counted WAM gap locations with +module-relative constructor byte spans, constructor/module hashes, and matching +bundle locations. It checks the same complete bundle set and parse coverage. +Two reasons may point at one construction; repeated copies of a module follow +the extractor's existing deduplication. The optional sidecar does not enlarge the +IR or change the existing `WamDiagnostics` API. Compare reports from the same +extractor revision for upstream changes, and from two revisions on one locked +set for extraction changes. Neither module names nor minified offsets establish +identity between different snapshots. + ## Classifying a change Every delta starts as `indeterminate`. A smaller count, a different source hash, diff --git a/scripts/lint-ir.py b/scripts/lint-ir.py index 34e52752..20f269f9 100755 --- a/scripts/lint-ir.py +++ b/scripts/lint-ir.py @@ -28,9 +28,10 @@ from collections import defaultdict from pathlib import Path -# Counted states, with the value observed when this guard was introduced. Raising -# one of these is a deliberate act: it means a constraint the extractor used to -# recover is now being lost, and the number has to be updated with a reason. +# Counted states are exact pins, not upper bounds. Changes in either direction +# require source attribution: modules may depart, new gaps may arrive, or extraction +# may improve or regress. The six pins reviewed for WA 2.3000.1047483476 are justified +# in docs/snapshot-quality-2026-10-07.md and its source/gap evidence sidecars. BASELINE = { "content integer with no byte width": 0, # An accessor that checks a value set whose out-of-set behaviour `wa_ir::wap` has not @@ -62,9 +63,9 @@ # and says nothing. Almost all of these are the legacy `WAWeb*Job` builders, which # take positional parameters and have no argument object to address at all; they fall # only if those builders stop existing or start being read. - "iq builder attribute with no argument path": 55, + "iq builder attribute with no argument path": 49, "iq builder content with no argument path": 23, - "iq builder child with no argument path": 12, + "iq builder child with no argument path": 10, # A request whose addressee is supplied at runtime — a group's own JID, a newsletter's # — and whose argument key the scan could not recover. Two, for different reasons. # `WAWebGroupInviteJob` is a legacy positional builder with no argument object to @@ -89,8 +90,8 @@ # all `RawWamEvent`, the generic envelope whose schema is supplied at runtime by # design. The third is held at zero: a written key that names no field of the event # means a write was attributed to the wrong construction, and none currently is. - "wam construction with an unread argument": 104, - "wam construction of an event with no catalog entry": 41, + "wam construction with an unread argument": 79, + "wam construction of an event with no catalog entry": 29, "wam written key naming no field of the event": 0, # Variable keys whose presence the mex extractor did not establish: a value expression # it does not judge (a call's return value, an `await`), or an operation whose call @@ -115,10 +116,10 @@ # was reading every call whose handle it could not follow as that operation's, and a # handle read off something the module was given - `t.handle`, or the binding a `var # h = t.handle` leaves - is not one the dependency says anything about. The three - # keys are `DebugLabyrinthRange`, whose handle IS its own memoised require and is - # simply not followed that far; they are withdrawn rather than published on a rule - # that also claims another operation's variables. - "mex variable with an undetermined presence": 108, + # keys were `DebugLabyrinthRange`, whose handle was its own memoised require, + # not followed that far. That module now departs; six departed keys and four new + # unresolved nodes account for 108 -> 106 (see the snapshot evidence ledger). + "mex variable with an undetermined presence": 106, # Operations where the verdict is `undetermined` for EVERY variable - no call site # was recovered, or the one that was writes nothing this classifier reads. Pinned # beside the per-key count because a single key regaining a verdict moves that number @@ -128,11 +129,10 @@ # alone they are indistinguishable, so the name says what is actually measured. # # Rose to 13 with the same change that took the per-key count to 108: all three of - # `DebugLabyrinthRange`'s variables were the withdrawn ones, so the operation joins - # the ones a consumer can say nothing about. This is the number to watch if the - # handle resolution learns to follow a memoised require through a nested body - - # that operation is the one it would win back. - "mex operation with no established variable presence": 13, + # `DebugLabyrinthRange`'s variables were withdrawn. The reviewed snapshot removes + # four wholly undetermined operations and adds SmartComposer's unsupported + # useMutation alias, so the current exact pin is 13 - 4 + 1 = 10. + "mex operation with no established variable presence": 10, # A `defineGlobal` entry whose channel list is written and unreadable. Held at zero # because the alternative to dropping it is publishing `["regular"]` over a policy WA # stated and we failed to read — so a rise here is a channel rule going missing, not diff --git a/scripts/tests/test_snapshot_baselines.py b/scripts/tests/test_snapshot_baselines.py new file mode 100644 index 00000000..181d972f --- /dev/null +++ b/scripts/tests/test_snapshot_baselines.py @@ -0,0 +1,112 @@ +"""Mutate the real snapshot: each reviewed baseline must still reject drift both ways.""" +import contextlib +import copy +import importlib.util +import io +import json +from pathlib import Path +import shutil +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('lint_ir', ROOT / 'scripts/lint-ir.py') +lint = importlib.util.module_from_spec(spec) +spec.loader.exec_module(lint) + + +def nodes(value): + if isinstance(value, dict): + yield value + for child in value.values(): + yield from nodes(child) + elif isinstance(value, list): + for child in value: + yield from nodes(child) + + +class ReviewedBaselines(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) / 'generated' + shutil.copytree(ROOT / 'generated', self.root) + + def run_lint(self): + output = io.StringIO() + with patch.object(sys, 'argv', ['lint-ir.py', str(self.root)]), contextlib.redirect_stdout(output): + status = lint.main() + return status, output.getvalue() + + def changed(self, path, original, mutate, name): + value = copy.deepcopy(original) + mutate(value) + (self.root / path).write_text(json.dumps(value)) + status, output = self.run_lint() + self.assertEqual(status, 1) + self.assertIn('CHANGED ' + name + ':', output) + (self.root / path).write_text(json.dumps(original)) + + def test_reviewed_snapshot_passes(self): + status, output = self.run_lint() + self.assertEqual(status, 0, output) + + def test_wam_gaps_still_fail_in_both_directions(self): + path = 'manifest.json' + original = json.loads((self.root / path).read_text()) + for key, name in [ + ('unreadConstructionArgument.identifier', 'wam construction with an unread argument'), + ('construction of an event with no catalog entry', 'wam construction of an event with no catalog entry'), + ]: + for delta in [-1, 1]: + with self.subTest(key=key, delta=delta): + def mutate(value): + drops = value['diagnostics']['wam']['dropsByReason'] + drops[key] += delta + self.changed(path, original, mutate, name) + + def test_iq_argument_gaps_still_fail_in_both_directions(self): + path = 'iq/index.json' + original = json.loads((self.root / path).read_text()) + for kind in ['attribute', 'child']: + for add_gap in [False, True]: + with self.subTest(kind=kind, add_gap=add_gap): + def mutate(value): + candidates = [] + for stanza in value['stanzas']: + for node in nodes(stanza.get('request', {}).get('children', [])): + if kind == 'attribute': + candidates += [a for a in node.get('attrs', []) if a.get('kind') not in ('const', 'generated_id') and 'value' not in a] + elif node.get('repeats') or node.get('presence', 'required') != 'required': + candidates.append(node) + node = next(n for n in candidates if ('argPath' in n) == add_gap) + if add_gap: + del node['argPath'] + else: + node['argPath'] = ['baseline_probe'] + self.changed(path, original, mutate, f'iq builder {kind} with no argument path') + + def test_mex_presence_gaps_still_fail_in_both_directions(self): + path = 'mex/index.json' + original = json.loads((self.root / path).read_text()) + for operation in [False, True]: + name = 'mex operation with no established variable presence' if operation else 'mex variable with an undetermined presence' + for add_gap in [False, True]: + with self.subTest(operation=operation, add_gap=add_gap): + def mutate(value): + for op in value['operations'].values(): + presence = op.get('variablesPresence', {}) + if operation: + if presence and all(n['presence'] == 'undetermined' for n in presence.values()) != add_gap: + for node in presence.values(): + node['presence'] = 'undetermined' if add_gap else 'always' + return + else: + for node in nodes(presence): + if 'presence' in node and (node['presence'] == 'undetermined') != add_gap: + node['presence'] = 'undetermined' if add_gap else 'always' + return + self.fail('no suitable presence node') + self.changed(path, original, mutate, name) From 440abf3a53760085d306444bf492f3fc4073f588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:34:01 +0000 Subject: [PATCH 18/52] test: qualify IQ binary-content fix across both snapshots --- .../tests/fixtures/independent_iq_cases.rs | 28 +++++++++++++++++++ tests/conformance/compiled/README.md | 17 +++++++++++ 2 files changed, 45 insertions(+) diff --git a/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs b/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs index ae41b9c5..38590f8f 100644 --- a/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs +++ b/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs @@ -290,3 +290,31 @@ fn binary_error_content_reaches_fallback() { "binary error content must reach fallback: {value:?}" ); } + +#[test] +fn absent_and_empty_binary_error_content_remain_distinct() { + let subject = specs().0; + for binary in [None, Some(vec![])] { + let mut response = error("406", Some("not-acceptable")); + response.children[0].bytes = binary.clone(); + let MakeSetSubjectRequestResponse::ClientError(value) = subject + .parse_response_with_request(&response.as_ref(), "req-1", "123@g.us") + .unwrap() + else { + panic!("client error required") + }; + let specific = matches!(value.error_set_subject_client_errors, + Some(MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorNotAcceptable(_))); + let fallback = matches!(value.error_set_subject_client_errors, + Some(MakeSetSubjectRequestClientErrorErrorSetSubjectClientErrors::IQErrorFallbackClient(_))); + assert_eq!(specific, binary.is_none(), "{value:?}"); + assert_eq!(fallback, binary.is_some(), "{value:?}"); + } + // Parsers that inspect only attributes must still accept binary content. + for bytes in [vec![], b"opaque".to_vec()] { + let mut response = error("499", Some("unknown")); + response.children[0].bytes = Some(bytes); + assert_eq!(subject_class(&response).unwrap(), "client"); + assert_eq!(accept_class(&response).unwrap(), "client"); + } +} diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index 1ad94a44..565d04ca 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -134,3 +134,20 @@ No change to their emitter is made here. After their fix, rerun this same failin case and the complete matrix before proposing a stacked qualification PR with explicit dependencies on both #51 and #53. Do not weaken the expected fallback or turn the failure into an ignored test. + +## IQ fix qualification + +Composition updated with IQ-owned fix +`9e609d4dee4c80ec50bc42a75110e69db13400d8` from #53. +The original seven case groups were rerun without changing their expectations: +both snapshots changed from six passing / one failing to seven passing / zero +failing. The binary 406 error now selects `IQErrorFallbackClient`. + +An additional independent group checks absent content versus empty binary +content and confirms attribute-only fallback accepts empty and nonempty bytes +for both pilots. The existing valid `field` child case still selects the specific +406 payload with its original name/reason. Both snapshots now pass all eight +groups, including deterministic regeneration and compiled execution. The combined +independent test phase took 0.79 s after build; #53's compiled runtime test also +passed. No original expected outcome was changed, no emitter was edited here, +and no test was disabled. Earlier failure evidence above refers to a863bfb. From 1d5d7f3323a6b71493a1bd68b3a326dfb468b361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:37:37 +0000 Subject: [PATCH 19/52] fix: include nested definitions in checked provenance index --- crates/wa-transform/src/lib.rs | 22 +++++++++++++++++++++- docs/snapshot-quality-2026-10-07.md | 3 +++ docs/snapshot-quality.md | 4 +++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/wa-transform/src/lib.rs b/crates/wa-transform/src/lib.rs index 525bdcac..58207086 100644 --- a/crates/wa-transform/src/lib.rs +++ b/crates/wa-transform/src/lib.rs @@ -40,6 +40,7 @@ pub fn extract_module_definitions(code: &str) -> Vec { let mut visitor = ModuleVisitor { modules: Vec::new(), + include_nested: false, }; visitor.visit_program(&ret.program); visitor.modules @@ -62,6 +63,7 @@ pub fn extract_module_definitions_checked( } let mut visitor = ModuleVisitor { modules: Vec::new(), + include_nested: true, }; visitor.visit_program(&ret.program); Ok(visitor.modules) @@ -69,6 +71,7 @@ pub fn extract_module_definitions_checked( struct ModuleVisitor { modules: Vec, + include_nested: bool, } impl<'a> Visit<'a> for ModuleVisitor { @@ -77,7 +80,13 @@ impl<'a> Visit<'a> for ModuleVisitor { // A module: record it and do NOT walk its children — `__d` never nests // inside another factory body, so skipping it avoids traversing the // (potentially huge) factory. - Some(def) => self.modules.push(def), + Some(def) => { + self.modules.push(def); + // Provenance must not turn a fast-path assumption into an absence claim. + if self.include_nested { + walk::walk_call_expression(self, call); + } + } // Non-`__d` call: keep walking so `__d` inside an IIFE wrapper is found. None => walk::walk_call_expression(self, call), } @@ -140,6 +149,17 @@ mod tests { assert!(extract_module_definitions_checked("").unwrap().is_empty()); } + #[test] + fn checked_index_includes_nested_static_definitions() { + let source = r#"__d("Outer",[],function(){__d("Inner",[],function(){});});"#; + let modules = extract_module_definitions_checked(source).unwrap(); + assert_eq!( + modules.iter().map(|m| m.name.as_str()).collect::>(), + ["Outer", "Inner"] + ); + assert_eq!(extract_module_definitions(source).len(), 1); + } + #[test] fn empty_input() { assert!(extract_module_definitions("").is_empty()); diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index f1336f69..3ad6d4bb 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -51,6 +51,9 @@ constructor spans/hashes, counter arithmetic, and remaining limitations. The modules in both locked sets. Every bundle parses without recovery: 20,209 old and 19,713 new exact static module names. This establishes coverage of static module definitions, not completeness of the server protocol or dynamic JavaScript. +A follow-up test exposed the extraction fast path skipping nested definitions. +The checked index now traverses them; both complete pinned indexes remain +byte-for-byte identical, so none of the absence findings depended on that shortcut. ## IQ evidence diff --git a/docs/snapshot-quality.md b/docs/snapshot-quality.md index 36c26df0..16363622 100644 --- a/docs/snapshot-quality.md +++ b/docs/snapshot-quality.md @@ -53,7 +53,9 @@ exclusive end, so invalid UTF-8 is rejected rather than decoded lossily. Every recovered occurrence remains visible, including conflicting definitions. The index is deterministic and contains no workspace-specific file paths. -The sidecar requires every locked bundle to parse without recovery and states +The checked index also visits nested static `__d` definitions; the extraction +fast path alone would skip them. The sidecar requires every locked bundle to +parse without recovery and states `coverage: parsed-static-definitions`. A missing exact name proves only that no matching static definition was recovered from these fully parsed inputs. It does not prove feature removal, dynamic-module absence, or a rename. A module hint in From 7ea45e4228e9228e9ba2f71991accea37826676b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:45:17 +0000 Subject: [PATCH 20/52] test: verify compiled fixture identities and preserve server parser --- tests/conformance/compiled/README.md | 20 +++++++++++ tests/conformance/compiled/capture_errors.py | 4 ++- .../WASmaxInGroupsBaseServerErrorMixin.js | 1 + .../sources/2.3000.1045368834/provenance.json | 7 ++++ .../WASmaxInGroupsBaseServerErrorMixin.js | 1 + .../sources/2.3000.1047483476/provenance.json | 7 ++++ tests/conformance/test_capture.py | 36 +++++++++++++------ 7 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsBaseServerErrorMixin.js create mode 100644 tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsBaseServerErrorMixin.js diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index 565d04ca..0d42ba03 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -151,3 +151,23 @@ groups, including deterministic regeneration and compiled execution. The combine independent test phase took 0.79 s after build; #53's compiled runtime test also passed. No original expected outcome was changed, no emitter was edited here, and no test was disabled. Earlier failure evidence above refers to a863bfb. + +## Integrated quality and fixture review + +The composition includes quality #52 at +`1d5d7f3323a6b71493a1bd68b3a326dfb468b361`, preserving its commits, +and audit #54 via the existing dependency commits. A follow-up quality fix for +external schema references is pending; this is not final qualification. + +Review identified two missing evidence checks. The offline CI fixture command now +checks both compiled source sets against their recorded AST lengths/hashes and +both selected inputs against provenance hashes and versions. Same-length source +and input mutations passed the former check and fail the extended check. This +verifies recorded identity, not authenticity of jointly edited provenance; full +recapture remains necessary. The selected records and complete historical IQ +hashes were also compared to their recorded git commits locally. + +Both verified bundle sets were recaptured with `WASmaxInGroupsBaseServerErrorMixin` +added, giving 20 modules per snapshot. Its body shows the unique error child, +IQ error response correlation and ordered ServerErrors parser delegation. All +20 module bodies are identical between snapshots. No JavaScript was executed. diff --git a/tests/conformance/compiled/capture_errors.py b/tests/conformance/compiled/capture_errors.py index 5426f553..586d0ec0 100644 --- a/tests/conformance/compiled/capture_errors.py +++ b/tests/conformance/compiled/capture_errors.py @@ -9,11 +9,12 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import capture -capture.MODULES = [ +MODULES = [ 'WASmaxInGroupsIQErrorResponseMixin', 'WASmaxInGroupsSetSubjectClientErrors', 'WASmaxInGroupsAcceptGroupAddClientErrors', 'WASmaxInGroupsServerErrors', + 'WASmaxInGroupsBaseServerErrorMixin', 'WASmaxInGroupsSetSubjectResponseClientError', 'WASmaxInGroupsAcceptGroupAddResponseClientError', 'WASmaxInGroupsSetSubjectResponseServerError', @@ -32,4 +33,5 @@ ] if __name__ == '__main__': + capture.MODULES = MODULES capture.capture(*(Path(arg) for arg in sys.argv[1:])) diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsBaseServerErrorMixin.js b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsBaseServerErrorMixin.js new file mode 100644 index 00000000..193a1905 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1045368834/WASmaxInGroupsBaseServerErrorMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsBaseServerErrorMixin",["WAResultOrError","WASmaxInGroupsIQErrorResponseMixin","WASmaxInGroupsServerErrors","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseUtils").flattenedChildWithTag(e,"error");if(!r.success)return r;var a=o("WASmaxInGroupsIQErrorResponseMixin").parseIQErrorResponseMixin(e,t);if(!a.success)return a;var i=o("WASmaxInGroupsServerErrors").parseServerErrors(r.value);return i.success?o("WAResultOrError").makeResult(babelHelpers.extends({},a.value,{errorServerErrors:i.value})):i}l.parseBaseServerErrorMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1045368834/provenance.json b/tests/conformance/compiled/sources/2.3000.1045368834/provenance.json index f0e3e372..c3a88082 100644 --- a/tests/conformance/compiled/sources/2.3000.1045368834/provenance.json +++ b/tests/conformance/compiled/sources/2.3000.1045368834/provenance.json @@ -25,6 +25,13 @@ "end": 5096, "sourceSha256": "0786f985a20352044f3f00cec811911fce0f002f4dedafdf3b37e1cd087cbab6" }, + { + "module": "WASmaxInGroupsBaseServerErrorMixin", + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "start": 36410, + "end": 37063, + "sourceSha256": "93013e540bfaf1466e8e6d37863c83ae57c85102c7d99ee1db819b08eb5f0a1e" + }, { "module": "WASmaxInGroupsIQErrorAlreadyExistsMixin", "bundleSha256": "cf6197f616a43cf9004101d911d5643e8fa63972b2d27a54e8bba8db062abb8f", diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsBaseServerErrorMixin.js b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsBaseServerErrorMixin.js new file mode 100644 index 00000000..193a1905 --- /dev/null +++ b/tests/conformance/compiled/sources/2.3000.1047483476/WASmaxInGroupsBaseServerErrorMixin.js @@ -0,0 +1 @@ +__d("WASmaxInGroupsBaseServerErrorMixin",["WAResultOrError","WASmaxInGroupsIQErrorResponseMixin","WASmaxInGroupsServerErrors","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e,t){var n=o("WASmaxParseUtils").assertTag(e,"iq");if(!n.success)return n;var r=o("WASmaxParseUtils").flattenedChildWithTag(e,"error");if(!r.success)return r;var a=o("WASmaxInGroupsIQErrorResponseMixin").parseIQErrorResponseMixin(e,t);if(!a.success)return a;var i=o("WASmaxInGroupsServerErrors").parseServerErrors(r.value);return i.success?o("WAResultOrError").makeResult(babelHelpers.extends({},a.value,{errorServerErrors:i.value})):i}l.parseBaseServerErrorMixin=e}),98); diff --git a/tests/conformance/compiled/sources/2.3000.1047483476/provenance.json b/tests/conformance/compiled/sources/2.3000.1047483476/provenance.json index 1cb17b9f..b8b9d525 100644 --- a/tests/conformance/compiled/sources/2.3000.1047483476/provenance.json +++ b/tests/conformance/compiled/sources/2.3000.1047483476/provenance.json @@ -25,6 +25,13 @@ "end": 5096, "sourceSha256": "0786f985a20352044f3f00cec811911fce0f002f4dedafdf3b37e1cd087cbab6" }, + { + "module": "WASmaxInGroupsBaseServerErrorMixin", + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "start": 36165, + "end": 36818, + "sourceSha256": "93013e540bfaf1466e8e6d37863c83ae57c85102c7d99ee1db819b08eb5f0a1e" + }, { "module": "WASmaxInGroupsIQErrorAlreadyExistsMixin", "bundleSha256": "7c7be0378ce2e7ff49b9f5909cf8a7d283000af82cc43bc071c2df3541ec5d56", diff --git a/tests/conformance/test_capture.py b/tests/conformance/test_capture.py index 59d97711..ab0d887f 100644 --- a/tests/conformance/test_capture.py +++ b/tests/conformance/test_capture.py @@ -7,6 +7,7 @@ from unittest.mock import patch from capture import MODULES, capture +from compiled.capture_errors import MODULES as COMPILED_MODULES class CaptureTests(unittest.TestCase): @@ -51,16 +52,31 @@ def test_single_quoted_and_escaped_names_reach_the_ast(self): self.capture_synthetic(spelling) def test_committed_sources_match_every_recorded_span_hash(self): - for version in ['2.3000.1045368834', '2.3000.1047483476']: - root = Path(__file__).parent / version - evidence = json.loads((root / 'provenance.json').read_text()) - self.assertEqual({e['module'] for e in evidence['modules']}, set(MODULES)) - for entry in evidence['modules']: - source = (root / (entry['module'] + '.js')).read_bytes() - self.assertTrue(source.endswith(b';\n')) - source = source[:-2] # Capture's terminator, not part of the AST span. - self.assertEqual(len(source), entry['end'] - entry['start']) - self.assertEqual(hashlib.sha256(source).hexdigest(), entry['sourceSha256']) + for base, modules in [(Path(__file__).parent, MODULES), + (Path(__file__).parent / 'compiled/sources', COMPILED_MODULES)]: + for version in ['2.3000.1045368834', '2.3000.1047483476']: + root = base / version + evidence = json.loads((root / 'provenance.json').read_text()) + self.assertEqual(evidence['waVersion'], version) + self.assertEqual({e['module'] for e in evidence['modules']}, set(modules)) + self.assertEqual({p.stem for p in root.glob('*.js')}, set(modules)) + for entry in evidence['modules']: + source = (root / (entry['module'] + '.js')).read_bytes() + self.assertTrue(source.endswith(b';\n')) + source = source[:-2] + self.assertEqual(len(source), entry['end'] - entry['start']) + self.assertEqual(hashlib.sha256(source).hexdigest(), entry['sourceSha256']) + + def test_compiled_inputs_match_recorded_hashes(self): + root = Path(__file__).parent / 'compiled/inputs' + evidence = json.loads((root / 'provenance.json').read_text()) + self.assertEqual({e['waVersion'] for e in evidence}, + {'2.3000.1045368834', '2.3000.1047483476'}) + self.assertEqual(len(evidence), 2) + for entry in evidence: + source = (root / (entry['waVersion'] + '.json')).read_bytes() + self.assertEqual(hashlib.sha256(source).hexdigest(), entry['selectedInputSha256']) + self.assertEqual(json.loads(source)['waVersion'], entry['waVersion']) def test_changed_missing_extra_and_wrong_set_hash_are_rejected_before_ast(self): for failure in ['changed', 'missing', 'extra', 'setHash', 'count']: From eaea5f9e5d864ee9d5ef4074709eddb9aba51735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:49:31 +0000 Subject: [PATCH 21/52] fix: validate schema reference closure before instances --- docs/snapshot-quality-2026-10-07.md | 9 +++- docs/snapshot-quality.md | 10 +++- scripts/tests/test_snapshot_validation.py | 47 +++++++++++++++++ scripts/validate-schemas.py | 62 +++++++++++++++++++++-- 4 files changed, 122 insertions(+), 6 deletions(-) diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index 3ad6d4bb..bde5f0ca 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -24,7 +24,14 @@ The `gh` API returned `Forbidden`; connected GitHub tools handled PR operations. Both historical snapshots pass all 12 emitted JSON schemas. The old validator incorrectly passed an empty directory; it now fails missing documents/schemas, invalid JSON/schema, and external references. It reports the number actually -validated. Nine validator regression tests cover those cases. +validated. Eleven validator regression tests cover those cases. + +A follow-up review reproduced another false success: an external `$ref` inside +an absent optional property or unused `$defs` passed with `{}`. Both new cases +fail on the previous validator. Validation now checks the complete structural +reference closure before checking the instance, using only embedded resources. +Draft-aware positive cases cover local pointers, anchors, nested IDs and recursive +references; annotation data remains opaque unless a reference targets it. Before the WAM extraction fix, the same release binary reproduced all 27 checked artifacts of both historical commits. Warm runs took 13.970 s / 13.114 s, with diff --git a/docs/snapshot-quality.md b/docs/snapshot-quality.md index 16363622..05774a64 100644 --- a/docs/snapshot-quality.md +++ b/docs/snapshot-quality.md @@ -3,7 +3,15 @@ The existing text `whatspec diff OLD NEW` is a count and name summary. Use `whatspec diff OLD NEW --json` for contract deltas across every manifest domain. The report checks domain hashes, document versions, and lock self-consistency. -It fails on missing declared artifacts. Run schema validation separately, because +It fails on missing declared artifacts. Schema validation preflights every +schema-valued location and referenced target against an in-memory registry, +including unused definitions and absent optional properties. Local pointers, +anchors and embedded resource IDs follow the declared draft; data in annotations +is not treated as a schema unless referenced. References needing an external +resource fail without network access. Unknown explicit drafts fail rather than +being silently interpreted as 2020-12. + +Run schema validation separately, because matching a manifest hash does not establish schema conformance. ```sh diff --git a/scripts/tests/test_snapshot_validation.py b/scripts/tests/test_snapshot_validation.py index 24230981..e6a35454 100644 --- a/scripts/tests/test_snapshot_validation.py +++ b/scripts/tests/test_snapshot_validation.py @@ -64,6 +64,53 @@ def test_wrong_shape_is_failure(self): (self.root / validator.DOMAINS[0][0]).write_text('[]') self.assertEqual(self.validate(), 1) + def test_unused_external_references_are_failure(self): + for schema in [ + {"type": "object", "properties": {"optional": {"$ref": "https://invalid.example/schema.json"}}}, + {"$defs": {"unused": {"$ref": "https://invalid.example/schema.json"}}}, + ]: + with self.subTest(schema=schema): + (self.root / validator.DOMAINS[0][1]).write_text(json.dumps(schema)) + self.assertEqual(self.validate(), 1) + + def test_reference_structure_respects_drafts_and_local_scopes(self): + cases = [ + # Forward local pointers/anchors, nested resource IDs, and recursion. + ({"$defs": {"node": {"$anchor": "node", "properties": {"next": {"$ref": "#node"}}}}, + "properties": {"optional": {"$ref": "#/$defs/node"}}}, 0), + ({"$id": "https://example.test/root", "$defs": { + "child": {"$id": "child", "$defs": {"leaf": {"$anchor": "leaf", "type": "string"}}, + "properties": {"x": {"$ref": "#leaf"}}}}, + "properties": {"optional": {"$ref": "child#leaf"}}}, 0), + ({"$defs": {"bad": {"$ref": "#/$defs/missing"}}}, 1), + ({"$defs": {"bad": {"$ref": "#missing"}}}, 1), + ({"$defs": {"bad": {"$dynamicRef": "https://invalid.example/schema.json"}}}, 1), + ({"$dynamicAnchor": "node", "$defs": {"node": {"$dynamicRef": "#node"}}}, 0), + ({"$schema": "http://json-schema.org/draft-07/schema#", + "definitions": {"unused": {"$ref": "https://invalid.example/schema.json"}}}, 1), + ({"$schema": "http://json-schema.org/draft-07/schema#", + "definitions": {"node": {"$id": "#node", "type": "object"}}, + "properties": {"optional": {"$ref": "#node"}}}, 0), + ({"$schema": "http://json-schema.org/draft-04/schema#", + "definitions": {"node": {"id": "#node", "type": "object"}}, + "properties": {"optional": {"$ref": "#node"}}}, 0), + ({"$schema": "https://json-schema.org/draft/2019-09/schema", + "$recursiveAnchor": True, "$defs": {"node": {"$recursiveRef": "#"}}}, 0), + ({"$schema": "http://json-schema.org/draft-07/schema#", + "$dynamicRef": "https://invalid.example/schema.json"}, 0), + ({"type": "object", "default": {"$ref": "https://invalid.example/schema.json"}, + "examples": [{"$ref": "https://invalid.example/schema.json"}], + "properties": {"optional": {"const": {"$ref": "https://invalid.example/schema.json"}}}}, 0), + ({"properties": {"optional": {"$ref": "#/default"}}, + "default": {"$ref": "https://invalid.example/schema.json"}}, 1), + ({"$schema": "https://invalid.example/unknown-draft"}, 1), + ({"$schema": []}, 1), + ] + for schema, expected in cases: + with self.subTest(schema=schema): + (self.root / validator.DOMAINS[0][1]).write_text(json.dumps(schema)) + self.assertEqual(self.validate(), expected) + def test_external_reference_is_failure_without_network(self): (self.root / validator.DOMAINS[0][1]).write_text('{"$ref":"https://invalid.example/schema.json"}') self.assertEqual(self.validate(), 1) diff --git a/scripts/validate-schemas.py b/scripts/validate-schemas.py index 03f057ab..58bf2ea3 100755 --- a/scripts/validate-schemas.py +++ b/scripts/validate-schemas.py @@ -13,7 +13,9 @@ try: from jsonschema import Draft202012Validator from jsonschema.exceptions import SchemaError - from referencing import Registry + from jsonschema.validators import validator_for + from referencing import Registry, Resource + from referencing.jsonschema import DRAFT202012, specification_with from referencing.exceptions import Unresolvable except ImportError: sys.exit("jsonschema not installed — `pip install jsonschema`") @@ -35,6 +37,59 @@ ] +def schema_validator(schema, default=Draft202012Validator): + if not isinstance(schema, dict) or "$schema" not in schema: + return default + if not isinstance(schema["$schema"], str): + raise ValueError("$schema must be a dialect URI string") + selected = validator_for(schema, default=None) + if selected is None: + raise ValueError(f"unsupported schema dialect: {schema['$schema']}") + return selected + + +def checked_registry(schema): + """Check every schema-valued location, including branches no instance visits. + + Resource.subresources follows the declared draft's schema keywords, so objects + in const/default/examples are data, not schemas. Registry has no retriever: + URI-shaped IDs may name embedded resources, but nothing is fetched. + """ + selected = schema_validator(schema) + selected.check_schema(schema) + root = Resource.from_contents(schema, default_specification=DRAFT202012) + registry = Registry().with_resource("", root).crawl() + pending = [(root, registry.resolver().in_subresource(root), selected)] + seen = set() + while pending: + resource, resolver, inherited = pending.pop() + contents = resource.contents + dialect = schema_validator(contents, inherited) + identity = (id(contents), dialect) + if identity in seen: + continue + seen.add(identity) + if isinstance(contents, dict): + for keyword in ("$ref", "$dynamicRef", "$recursiveRef"): + if keyword in dialect.VALIDATORS and keyword in contents: + resolved = resolver.lookup(contents[keyword]) + # A pointer must lead to a schema, not an arbitrary JSON value. + target_dialect = schema_validator(resolved.contents, dialect) + target_dialect.check_schema(resolved.contents) + target = Resource.from_contents( + resolved.contents, + default_specification=specification_with( + target_dialect.ID_OF(target_dialect.META_SCHEMA)), + ) + # A ref can turn an otherwise opaque annotation into a schema. + pending.append((target, resolved.resolver, target_dialect)) + pending.extend( + (child, resolver.in_subresource(child), dialect) + for child in resource.subresources() + ) + return selected, registry + + def validate(root: Path) -> int: failures = 0 for doc_rel, schema_rel in DOMAINS: @@ -46,9 +101,8 @@ def validate(root: Path) -> int: try: schema = json.loads(schema_path.read_text()) document = json.loads(doc_path.read_text()) - Draft202012Validator.check_schema(schema) - # No remote schema retrieval: emitted schemas must be self-contained. - validator = Draft202012Validator(schema, registry=Registry()) + selected, registry = checked_registry(schema) + validator = selected(schema, registry=registry) errors = sorted(validator.iter_errors(document), key=lambda e: tuple(str(p) for p in e.path)) except (OSError, ValueError, SchemaError, Unresolvable) as err: From cd0f9dad15628cf844c367bb6a2a1a086ed2d6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:54:05 +0000 Subject: [PATCH 22/52] test: run quality regressions in composed CI and record joint evidence --- .github/workflows/ci.yml | 3 ++ tests/conformance/compiled/README.md | 42 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1992ca29..55c1262b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,9 @@ jobs: pip install --quiet jsonschema python scripts/validate-schemas.py generated + - name: Test snapshot validation and reviewed baselines + run: python3 -m unittest discover -s scripts/tests -v + # The schemas check SHAPE; this checks that the document is usable. A # consumer generating code from the IR — in any language — has to trust # that an enum names its values, a byte pin matches its length, a union diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index 0d42ba03..d7702571 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -171,3 +171,45 @@ Both verified bundle sets were recaptured with `WASmaxInGroupsBaseServerErrorMix added, giving 20 modules per snapshot. Its body shows the unique error child, IQ error response correlation and ordered ServerErrors parser delegation. All 20 module bodies are identical between snapshots. No JavaScript was executed. + +## Joint qualification through schema closure fix + +Quality #52 at `eaea5f9e5d864ee9d5ef4074709eddb9aba51735` is now +integrated with original ancestry. The follow-up changes Python validation and +its tests/docs, not Rust extraction or code generation. Its 15 Python tests pass, +including references in unvisited branches. The composition workflow now runs +`python3 -m unittest discover -s scripts/tests -v` after installing jsonschema; +the separate conformance-fixtures job remains unchanged. + +Local workspace tests with all features passed: 1,657 tests, none failed or +ignored. This includes the two compiled independent snapshot suites and the IQ +owner's runtime suite. All-feature/all-target Clippy and formatting passed. +The committed 12 schema/document pairs, all exact lint baselines and five fixture +tests pass. The current snapshot regenerated twice with all 35 files identical: +15,384,513 bytes, 45.132 s and 51.543 s. The committed 27-artifact `--check` passed +in 53.467 s. These are development-build observations, not comparable performance +benchmarks against the earlier release-build baseline. + +CI run 37698190460 on 7ea45e4 passed determinism and fixture checks, but failed +before running Rust tests: Cargo could not remove `target/debug/examples/gen` +with ENOENT. Its log also identifies output-name collisions among the existing +`gen` examples in wa-appstate, wa-codegen, wa-mex and wa-proto. This is not a +conformance assertion failure; it remains an infrastructure issue to coordinate. +A local default-feature build separately exhausted disk space; only this +composition's build cache was removed, and the retry disables debug symbols. + +The three fixture-review threads have evidence replies and are resolved. +Further quality findings about local objects in merges and overlapping module +definitions remain under investigation by their owner. Do not declare complete +stability from this qualification. No other PR branch has been modified. + +The default-feature retry completed with 1,657 passing tests, zero failures and +zero ignored tests. Both snapshots still pass all eight independent compiled +case groups. The old snapshot also regenerated twice identically: 35 files, +15,307,071 bytes. Runs took 43.237 s and 83.053 s; the latter overlapped the clean +Rust build after disk recovery. Its 12 schema/document pairs pass the updated +validator. The interrupted generation was not counted as a successful repeat. + +Recommended integration order is #54, #52, #51 and #53, followed by #55's remaining +qualification changes after retargeting/reviewing its diff. Preserve ancestry and +avoid redistributing the composition commits into the individual workstreams. From 810a81bee8e35191b8b036d7339acc4fae5c43c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:55:19 +0000 Subject: [PATCH 23/52] fix: avoid overlapping WAM scans in gap reports --- crates/whatspec/src/quality_gaps.rs | 37 +++++++++++++++++++++++++++-- docs/snapshot-quality.md | 4 +++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/whatspec/src/quality_gaps.rs b/crates/whatspec/src/quality_gaps.rs index 6fea6dca..4d0a53fe 100644 --- a/crates/whatspec/src/quality_gaps.rs +++ b/crates/whatspec/src/quality_gaps.rs @@ -21,8 +21,7 @@ pub fn run(args: &[String]) -> Result<()> { observed.set_hash == lock.set_hash && observed.bundle_count == lock.bundle_count, "bundle set differs from lock" ); - let defs = wa_transform::extract_module_definitions_checked(&source) - .map_err(|errors| anyhow::anyhow!("bundle set parse failed: {}", errors.join("; ")))?; + let defs = counted_definitions(&source)?; let by_start: BTreeMap<_, _> = defs.iter().map(|d| (d.start, d)).collect(); let (_, diag, gaps) = wa_wam::extract_wam_with_gap_sites(&source, &defs, &lock.wa_version); let mut sites = Vec::new(); @@ -54,3 +53,37 @@ pub fn run(args: &[String]) -> Result<()> { ); Ok(()) } + +fn counted_definitions(source: &str) -> Result> { + let mut defs = wa_transform::extract_module_definitions_checked(source) + .map_err(|errors| anyhow::anyhow!("bundle set parse failed: {}", errors.join("; ")))?; + // Match normal extraction's outer definitions: the WAM scanner already visits + // nested bodies. Keep the full nested index separately for source lookup. + let mut end = 0; + defs.retain(|definition| { + if definition.start < end { + false + } else { + end = definition.end; + true + } + }); + Ok(defs) +} + +#[cfg(test)] +mod tests { + #[test] + fn nested_definitions_do_not_double_count_wam_gaps() { + let source = r#"__d("Outer",["WAWebRawWamEvent"],function(){__d("Inner",["WAWebRawWamEvent"],function(){new(o("WAWebRawWamEvent")).RawWamEvent(unknown);});});"#; + let normal = wa_transform::extract_module_definitions(source); + let complete = wa_transform::extract_module_definitions_checked(source).unwrap(); + assert_eq!(complete.len(), 2); + let counted = super::counted_definitions(source).unwrap(); + let (_, expected) = wa_wam::extract_wam_from_modules(source, &normal, "test"); + let (_, observed, gaps) = wa_wam::extract_wam_with_gap_sites(source, &counted, "test"); + assert_eq!(observed.drops_by_reason, expected.drops_by_reason); + assert_eq!(gaps.len(), 2); + assert_eq!(counted, normal); + } +} diff --git a/docs/snapshot-quality.md b/docs/snapshot-quality.md index 05774a64..29019007 100644 --- a/docs/snapshot-quality.md +++ b/docs/snapshot-quality.md @@ -75,7 +75,9 @@ This keeps recoverable provenance outside every repeated field of the IR. module-relative constructor byte spans, constructor/module hashes, and matching bundle locations. It checks the same complete bundle set and parse coverage. Two reasons may point at one construction; repeated copies of a module follow -the extractor's existing deduplication. The optional sidecar does not enlarge the +the extractor's existing deduplication. Counted diagnostics use the same outer +module selection as normal extraction, avoiding overlapping scans of nested +definitions. The provenance index still retains nested definitions. The optional sidecar does not enlarge the IR or change the existing `WamDiagnostics` API. Compare reports from the same extractor revision for upstream changes, and from two revisions on one locked set for extraction changes. Neither module names nor minified offsets establish From 20d1efa1bfa78e42a620805a9e5ba8d275bcb07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 22:57:27 +0000 Subject: [PATCH 24/52] fix: give workspace generation examples unique Cargo output names --- .../examples/{gen.rs => gen_appstate.rs} | 6 ++-- .../wa-codegen/examples/{gen.rs => gen_iq.rs} | 8 ++--- crates/wa-mex/examples/{gen.rs => gen_mex.rs} | 4 +-- .../examples/{gen.rs => gen_proto.rs} | 6 ++-- tests/conformance/compiled/README.md | 30 +++++++++++++++++++ 5 files changed, 44 insertions(+), 10 deletions(-) rename crates/wa-appstate/examples/{gen.rs => gen_appstate.rs} (70%) rename crates/wa-codegen/examples/{gen.rs => gen_iq.rs} (72%) rename crates/wa-mex/examples/{gen.rs => gen_mex.rs} (88%) rename crates/wa-proto/examples/{gen.rs => gen_proto.rs} (70%) diff --git a/crates/wa-appstate/examples/gen.rs b/crates/wa-appstate/examples/gen_appstate.rs similarity index 70% rename from crates/wa-appstate/examples/gen.rs rename to crates/wa-appstate/examples/gen_appstate.rs index 89119817..6bf3b37a 100644 --- a/crates/wa-appstate/examples/gen.rs +++ b/crates/wa-appstate/examples/gen_appstate.rs @@ -1,9 +1,11 @@ //! Extract AppState schemas from bundle file(s) (concatenated) and print JSON. -//! Run: cargo run -p wa-appstate --example gen -- ... +//! Run: cargo run -p wa-appstate --example gen_appstate -- ... fn main() -> anyhow::Result<()> { let mut args = std::env::args().skip(1); - let version = args.next().expect("usage: gen ..."); + let version = args + .next() + .expect("usage: gen_appstate ..."); let mut source = String::new(); for path in args { source.push_str(&std::fs::read_to_string(&path)?); diff --git a/crates/wa-codegen/examples/gen.rs b/crates/wa-codegen/examples/gen_iq.rs similarity index 72% rename from crates/wa-codegen/examples/gen.rs rename to crates/wa-codegen/examples/gen_iq.rs index 133f5219..578c0c85 100644 --- a/crates/wa-codegen/examples/gen.rs +++ b/crates/wa-codegen/examples/gen_iq.rs @@ -1,14 +1,14 @@ //! Scan a bundle for IQ stanzas and generate the Rust `IqSpec` file. -//! Run: cargo run -p wa-codegen --example gen -- -//! cargo run -p wa-codegen --example gen -- --ir (dump IqIr JSON) +//! Run: cargo run -p wa-codegen --example gen_iq -- +//! cargo run -p wa-codegen --example gen_iq -- --ir (dump IqIr JSON) fn main() -> anyhow::Result<()> { let bundle = std::env::args() .nth(1) - .expect("usage: gen "); + .expect("usage: gen_iq "); let target = std::env::args() .nth(2) - .expect("usage: gen "); + .expect("usage: gen_iq "); let source = std::fs::read_to_string(&bundle)?; let scan = wa_scan::scan_iq_stanzas(&source); diff --git a/crates/wa-mex/examples/gen.rs b/crates/wa-mex/examples/gen_mex.rs similarity index 88% rename from crates/wa-mex/examples/gen.rs rename to crates/wa-mex/examples/gen_mex.rs index 4177f047..1fdf9f00 100644 --- a/crates/wa-mex/examples/gen.rs +++ b/crates/wa-mex/examples/gen_mex.rs @@ -1,11 +1,11 @@ //! Extract Mex persisted operations from one or more bundle files (concatenated). -//! Run: cargo run -p wa-mex --example gen -- ... [--json] +//! Run: cargo run -p wa-mex --example gen_mex -- ... [--json] fn main() -> anyhow::Result<()> { let mut args = std::env::args().skip(1); let version = args .next() - .expect("usage: gen ... [--json]"); + .expect("usage: gen_mex ... [--json]"); let mut source = String::new(); let mut json = false; for a in args { diff --git a/crates/wa-proto/examples/gen.rs b/crates/wa-proto/examples/gen_proto.rs similarity index 70% rename from crates/wa-proto/examples/gen.rs rename to crates/wa-proto/examples/gen_proto.rs index eabc41a8..df68b4fb 100644 --- a/crates/wa-proto/examples/gen.rs +++ b/crates/wa-proto/examples/gen_proto.rs @@ -1,9 +1,11 @@ //! Generate WAProto.proto from one or more bundle files (concatenated). -//! Run: cargo run -p wa-proto --example gen -- ... +//! Run: cargo run -p wa-proto --example gen_proto -- ... fn main() -> anyhow::Result<()> { let mut args = std::env::args().skip(1); - let version = args.next().expect("usage: gen ..."); + let version = args + .next() + .expect("usage: gen_proto ..."); let mut source = String::new(); for path in args { source.push_str(&std::fs::read_to_string(&path)?); diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index d7702571..7d7bec85 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -213,3 +213,33 @@ validator. The interrupted generation was not counted as a successful repeat. Recommended integration order is #54, #52, #51 and #53, followed by #55's remaining qualification changes after retargeting/reviewing its diff. Preserve ancestry and avoid redistributing the composition commits into the individual workstreams. + +## Cargo example output collision repair + +The CI failure is supported by Cargo's own collision diagnostics, not classified +as an unexplained flaky test. `cargo metadata --no-deps --format-version 1` on +cd0f9da reports four auto-discovered example targets named `gen`, owned by +wa-appstate, wa-codegen, wa-mex and wa-proto. All use the same output path +`target/debug/examples/gen`; run 37698190460 then fails removing that path with +ENOENT during the parallel workspace build. + +Each example now has a unique filename/target. No manifest or concurrency setting +changes are needed. Invocation migration, with all remaining arguments unchanged: + +| Package | Previous example | New example | +| --- | --- | --- | +| wa-appstate | `--example gen` | `--example gen_appstate` | +| wa-codegen | `--example gen` | `--example gen_iq` | +| wa-mex | `--example gen` | `--example gen_mex` | +| wa-proto | `--example gen` | `--example gen_proto` | + +The source command documentation and usage messages follow the new names. +Extraction/generation logic is unchanged. Old target-name aliases are not retained +because they would recreate the shared output path. These are developer examples; +no library API or generated contract changes. + +Validation: a uniqueness check over Cargo metadata fails on cd0f9da with exactly +those four targets and passes after the rename. `cargo build --workspace --examples +--message-format=json` reports four distinct executable paths. Parallel +`cargo test --workspace` passes 1,657 tests with zero failures/ignored tests and no +output-collision warning. Parallelism was not reduced. From 1a96e67e4c82681ebea4faba66433ab9b41dcc41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:04:22 +0000 Subject: [PATCH 25/52] fix: recover merge locals and trace unknown WAM fields --- crates/wa-wam/src/callsites.rs | 28 +++- crates/wa-wam/src/callsites/arguments.rs | 3 +- crates/wa-wam/src/lib.rs | 8 + crates/wa-wam/src/tests.rs | 93 ++++++++++++ crates/whatspec/src/quality_gaps.rs | 2 +- generated/manifest.json | 12 +- generated/wam/index.json | 177 ++++++++++++++++++++++- 7 files changed, 303 insertions(+), 20 deletions(-) diff --git a/crates/wa-wam/src/callsites.rs b/crates/wa-wam/src/callsites.rs index f38990f1..f340f414 100644 --- a/crates/wa-wam/src/callsites.rs +++ b/crates/wa-wam/src/callsites.rs @@ -294,7 +294,14 @@ impl<'a> Visit<'a> for SiteVisitor<'_> { let mut fields = Vec::new(); let mut partial = false; let mut unread = None; - read_argument(arg, &mut fields, &mut partial, &mut unread, self.aliases); + read_argument( + arg, + &mut fields, + &mut partial, + &mut unread, + self.aliases, + None, + ); let declared_in = binding_name(&base).and_then(|n| self.resolve(n)); for (field, _, value) in fields { self.writes.push(RawWrite { @@ -331,17 +338,13 @@ impl<'a> Visit<'a> for SiteVisitor<'_> { match n.arguments.first().and_then(wa_oxc::arg_expr) { // `new (…)(…)` with no argument: a real, complete field set of zero. None => {} - Some(arg) if self.arguments.contains_key(&arg.span().start) => { - let recovered = &self.arguments[&arg.span().start]; - fields = recovered.fields.clone(); - partial = recovered.partial; - } Some(arg) => read_argument( arg, &mut fields, &mut partial, &mut unread_argument, self.aliases, + Some(self.arguments), ), } self.sites.push(RawSite { @@ -368,7 +371,13 @@ fn read_argument( partial: &mut bool, unread: &mut Option<&'static str>, aliases: &RequireAliases, + locals: Option<&BTreeMap>, ) { + if let Some(recovered) = locals.and_then(|values| values.get(&arg.span().start)) { + fields.extend(recovered.fields.iter().cloned()); + *partial |= recovered.partial; + return; + } if let Some(obj) = as_object(arg) { for prop in &obj.properties { match prop { @@ -394,8 +403,11 @@ fn read_argument( { for a in &call.arguments { match a.as_expression() { - Some(e) if as_object(e).is_some() => { - read_argument(e, fields, partial, unread, aliases) + Some(e) + if as_object(e).is_some() + || locals.is_some_and(|values| values.contains_key(&e.span().start)) => + { + read_argument(e, fields, partial, unread, aliases, locals) } _ => *partial = true, } diff --git a/crates/wa-wam/src/callsites/arguments.rs b/crates/wa-wam/src/callsites/arguments.rs index 4b0d2984..817d61e3 100644 --- a/crates/wa-wam/src/callsites/arguments.rs +++ b/crates/wa-wam/src/callsites/arguments.rs @@ -92,6 +92,7 @@ impl Collector<'_> { &mut partial, &mut unread, self.aliases, + None, ); self.candidates.insert( (start, name.to_string()), @@ -168,7 +169,7 @@ impl<'a> Visit<'a> for Collector<'_> { } fn visit_call_expression(&mut self, c: &CallExpression<'a>) { // Direct eval could mutate any visible binding without an AST reference. - if wa_oxc::as_identifier(&c.callee) == Some("eval") { + if wa_oxc::as_identifier(super::unparen(&c.callee)) == Some("eval") { for candidate in self.candidates.values_mut() { candidate.valid = false; } diff --git a/crates/wa-wam/src/lib.rs b/crates/wa-wam/src/lib.rs index f8bb782c..882e4e2b 100644 --- a/crates/wa-wam/src/lib.rs +++ b/crates/wa-wam/src/lib.rs @@ -66,6 +66,8 @@ pub struct WamGapSite { pub event_module: String, pub event_export: String, pub reason: String, + /// Written key when this location describes a catalog mismatch. + pub field: Option, } /// What the scan recovered and, more importantly, what it did not. @@ -363,6 +365,7 @@ fn collect_call_sites( event_module: raw.event_module.clone(), event_export: raw.export.clone(), reason, + field: None, }; if let Some(form) = raw.unread_argument { if let Some(sites) = gap_sites.as_deref_mut() { @@ -422,6 +425,11 @@ fn collect_call_sites( // Anything else is a write we attributed wrongly, or a field the // catalog does not know. Either way it is not schema to publish, and // the site does write something this list cannot name. + if let Some(sites) = gap_sites.as_deref_mut() { + let mut site = gap(WRITTEN_KEY_NO_FIELD.to_string()); + site.field = Some(name.clone()); + sites.push(site); + } *diag .drops_by_reason .entry(WRITTEN_KEY_NO_FIELD.to_string()) diff --git a/crates/wa-wam/src/tests.rs b/crates/wa-wam/src/tests.rs index b1972c94..de2ff37d 100644 --- a/crates/wa-wam/src/tests.rs +++ b/crates/wa-wam/src/tests.rs @@ -783,6 +783,7 @@ fn mutable_escaped_shadowed_and_conditionally_initialized_arguments_stay_partial "function send(){var fields={retryCount:3}; function inner(){new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields); var fields;} inner();}", "function send(){var fields={retryCount:3}; fields={deviceCount:1}; new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", "function send(){var fields={retryCount:3}; function inner(){new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);} inner();}", + "function send(){var fields={retryCount:3}; (eval)('fields={}'); new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", "function send(){var fields={retryCount:3}; eval('fields={}'); new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(fields);}", ] { let (ir, _) = run_full(&format!( @@ -831,3 +832,95 @@ fn pinned_status_subtitle_refactor_preserves_attribution_fields() { ); assert!(new.call_sites.iter().any(|s| s.partial)); } + +#[test] +fn approved_locals_are_recovered_inside_fresh_target_merges() { + let source = format!( + r#"{LOCAL_ARGUMENT_CATALOG}__d("Reporter",["WAWebMessageSendWamEvent"],function(){{ + function send(){{var fields={{retryCount:3}}; + new(o("WAWebMessageSendWamEvent")).MessageSendWamEvent(babelHelpers.extends({{}},fields,{{deviceCount:1}})); + }} + }});"# + ); + let (ir, _) = run_full(&source); + let site = &ir + .events + .iter() + .find(|e| e.name == "MessageSend") + .unwrap() + .call_sites[0]; + assert!(!site.partial); + assert_eq!( + site.fields + .iter() + .map(|f| f.name.as_str()) + .collect::>(), + ["deviceCount", "retryCount"] + ); +} + +#[test] +fn mutated_merge_targets_and_escaped_sources_stay_partial() { + for body in [ + "var fields={retryCount:3}; new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(babelHelpers.extends(fields,{deviceCount:1}));", + "var fields={retryCount:3}; escape(fields); new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(babelHelpers.extends({},fields));", + "var fields={retryCount:3}; function inner(){new(o('WAWebMessageSendWamEvent')).MessageSendWamEvent(babelHelpers.extends({},fields));} inner();", + ] { + let source = format!( + "{LOCAL_ARGUMENT_CATALOG}__d(\"Reporter\",[\"WAWebMessageSendWamEvent\"],function(){{function send(){{{body}}}}});" + ); + let (ir, _) = run_full(&source); + let site = &ir + .events + .iter() + .find(|e| e.name == "MessageSend") + .unwrap() + .call_sites[0]; + assert!(site.partial, "{body}"); + assert!( + !site.fields.iter().any(|f| f.name == "retryCount"), + "{body}" + ); + } +} + +#[test] +fn approved_merge_fields_survive_an_unread_operand_without_claiming_completeness() { + let source = format!( + r#"{LOCAL_ARGUMENT_CATALOG}__d("Reporter",["WAWebMessageSendWamEvent"],function(){{ + function send(){{var fields={{retryCount:3}}; + new(o("WAWebMessageSendWamEvent")).MessageSendWamEvent(babelHelpers.extends({{}},fields,unknown())); + }} + }});"# + ); + let (ir, _) = run_full(&source); + let site = &ir + .events + .iter() + .find(|e| e.name == "MessageSend") + .unwrap() + .call_sites[0]; + assert!(site.partial); + assert_eq!(site.fields.len(), 1); + assert_eq!(site.fields[0].name, "retryCount"); +} + +#[test] +fn unknown_field_gaps_have_one_location_per_counted_write() { + let source = format!( + r#"{LOCAL_ARGUMENT_CATALOG}__d("Reporter",["WAWebMessageSendWamEvent"],function(){{ + new(o("WAWebMessageSendWamEvent")).MessageSendWamEvent({{notInCatalog:1,alsoUnknown:2}}); + }});"# + ); + let defs = wa_transform::extract_module_definitions(&source); + let (_, diag, gaps) = extract_wam_with_gap_sites(&source, &defs, "test"); + let reason = "written key naming no field of the event"; + assert_eq!(diag.drops_by_reason[reason], 2); + assert_eq!(gaps.iter().filter(|g| g.reason == reason).count(), 2); + assert_eq!( + gaps.iter() + .filter_map(|g| g.field.as_deref()) + .collect::>(), + std::collections::BTreeSet::from(["notInCatalog", "alsoUnknown"]) + ); +} diff --git a/crates/whatspec/src/quality_gaps.rs b/crates/whatspec/src/quality_gaps.rs index 4d0a53fe..08abad27 100644 --- a/crates/whatspec/src/quality_gaps.rs +++ b/crates/whatspec/src/quality_gaps.rs @@ -41,7 +41,7 @@ pub fn run(args: &[String]) -> Result<()> { "construction module is absent from verified source index" ); sites.push(json!({"sources":sources,"module":gap.module,"moduleSha256":wa_text::sha256_hex(module.as_bytes()), - "start":gap.start,"end":gap.end,"eventModule":gap.event_module,"eventExport":gap.event_export,"reason":gap.reason, + "start":gap.start,"end":gap.end,"eventModule":gap.event_module,"eventExport":gap.event_export,"reason":gap.reason,"field":gap.field, "constructionSha256":wa_text::sha256_hex(&module.as_bytes()[gap.start as usize..gap.end as usize])})); } sites.sort_by_key(|v| serde_json::to_string(v).unwrap()); diff --git a/generated/manifest.json b/generated/manifest.json index 67df1b24..2ee691aa 100644 --- a/generated/manifest.json +++ b/generated/manifest.json @@ -88,9 +88,9 @@ "types": 27 }, "wam": { - "callSiteFieldValues": 796, - "callSiteFields": 3446, - "callSites": 812, + "callSiteFieldValues": 804, + "callSiteFields": 3478, + "callSites": 813, "constants": 6, "constructions": 874, "dropsByReason": { @@ -99,10 +99,10 @@ "unreadConstructionArgument.identifier": 75, "unreadConstructionArgument.member": 3 }, - "duplicateCallSites": 33, + "duplicateCallSites": 32, "events": 439, "globals": 46, - "partialCallSites": 114, + "partialCallSites": 113, "privateStatsIds": 9, "samplingWeightOverrides": 4 } @@ -165,7 +165,7 @@ "wam": { "file": "wam/index.json", "schema": "schema/wam.schema.json", - "sha256": "d92a4cc88baeeaa9a2a9d8695371d01969b8025de589a605f8f33569a87abbbd" + "sha256": "9c6b0dd3069c3d84b6bedd6223f502b4bfa36be236340b4a08f2c2ed61483bec" }, "wasm": { "file": "wasm/index.json", diff --git a/generated/wam/index.json b/generated/wam/index.json index ff4b34d1..84d60b18 100644 --- a/generated/wam/index.json +++ b/generated/wam/index.json @@ -39432,6 +39432,22 @@ { "module": "WAWebKeepInChatMsgAction", "fields": [ + { + "name": "canEditDmSettings", + "write": "constructor" + }, + { + "name": "chatEphemeralityDuration", + "write": "constructor" + }, + { + "name": "isAGroup", + "write": "constructor" + }, + { + "name": "isAdmin", + "write": "constructor" + }, { "name": "keptCount", "write": "constructor" @@ -39449,6 +39465,18 @@ "key": "UNKEEP_MESSAGE" } }, + { + "name": "kicActor", + "write": "constructor" + }, + { + "name": "kicEntryPoint", + "write": "constructor" + }, + { + "name": "mediaType", + "write": "constructor" + }, { "name": "messageExpiredOnUnkeep", "write": "constructor" @@ -39456,13 +39484,40 @@ { "name": "messageExpiryTimer", "write": "constructor" + }, + { + "name": "messagesSelected", + "write": "constructor", + "value": { + "kind": "int", + "value": 1 + } + }, + { + "name": "threadId", + "write": "constructor" } - ], - "partial": true + ] }, { "module": "WAWebKeepInChatMsgAction", "fields": [ + { + "name": "canEditDmSettings", + "write": "constructor" + }, + { + "name": "chatEphemeralityDuration", + "write": "constructor" + }, + { + "name": "isAGroup", + "write": "constructor" + }, + { + "name": "isAdmin", + "write": "constructor" + }, { "name": "keptCount", "write": "constructor" @@ -39475,9 +39530,32 @@ "module": "WAWebWamEnumKicActionNameType", "key": "KEEP_MESSAGE" } + }, + { + "name": "kicActor", + "write": "constructor" + }, + { + "name": "kicEntryPoint", + "write": "constructor" + }, + { + "name": "mediaType", + "write": "constructor" + }, + { + "name": "messagesSelected", + "write": "constructor", + "value": { + "kind": "int", + "value": 1 + } + }, + { + "name": "threadId", + "write": "constructor" } - ], - "partial": true + ] }, { "module": "WAWebKeptDrawer.react", @@ -47631,6 +47709,97 @@ "callSites": [ { "module": "WAWebSmbMarkAsXOrderAction", + "fields": [ + { + "name": "ctwaOrderSignalVersion", + "write": "constructor", + "value": { + "kind": "int", + "value": 1 + } + }, + { + "name": "customerAdsSharingSettingEnabled", + "write": "constructor" + }, + { + "name": "deepLinkConversionSource", + "write": "constructor" + }, + { + "name": "orderPaid", + "write": "constructor" + }, + { + "name": "orderSignalType", + "write": "constructor", + "value": { + "kind": "enumMember", + "module": "WAWebWamEnumOrderSignalType", + "key": "UPDATED" + } + }, + { + "name": "orderStatus", + "write": "constructor" + }, + { + "name": "threadIdHmac", + "write": "constructor" + } + ], + "partial": true + }, + { + "module": "WAWebSmbMarkAsXOrderAction", + "fields": [ + { + "name": "ctwaOrderSignalVersion", + "write": "constructor", + "value": { + "kind": "int", + "value": 1 + } + }, + { + "name": "customerAdsSharingSettingEnabled", + "write": "constructor" + }, + { + "name": "deepLinkConversionSource", + "write": "constructor" + }, + { + "name": "orderPaid", + "write": "constructor", + "value": { + "kind": "bool", + "value": false + } + }, + { + "name": "orderSignalType", + "write": "constructor", + "value": { + "kind": "enumMember", + "module": "WAWebWamEnumOrderSignalType", + "key": "CREATED" + } + }, + { + "name": "orderStatus", + "write": "constructor", + "value": { + "kind": "enumMember", + "module": "WAWebWamEnumOrderStatus", + "key": "PROCESSING" + } + }, + { + "name": "threadIdHmac", + "write": "constructor" + } + ], "partial": true } ] From d6985d24df60a6e94b0f982bfd7416ed0581f10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:04:22 +0000 Subject: [PATCH 26/52] fix: include declared schemas in contract comparisons --- crates/whatspec/src/contract_diff.rs | 115 +++++++++++++++++++-------- 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/crates/whatspec/src/contract_diff.rs b/crates/whatspec/src/contract_diff.rs index 147da1d3..2680bab6 100644 --- a/crates/whatspec/src/contract_diff.rs +++ b/crates/whatspec/src/contract_diff.rs @@ -27,6 +27,7 @@ struct Snapshot { manifest: Value, lock: wa_store::lock::BundleLock, files: BTreeMap, + schemas: BTreeMap, } impl Snapshot { @@ -44,6 +45,7 @@ impl Snapshot { .context("manifest.domains missing")?; ensure!(!domains.is_empty(), "manifest.domains empty"); let mut files = BTreeMap::new(); + let mut schemas = BTreeMap::new(); for (domain, entry) in domains { let rel = entry["file"].as_str().context("domain file missing")?; let bytes = local_file(root, rel)?; @@ -69,11 +71,24 @@ impl Snapshot { json!({"artifactSha256": hash}) }; files.insert(domain.clone(), (rel.to_string(), hash, value)); + if let Some(schema) = entry.get("schema") { + let rel = schema + .as_str() + .context("declared schema path is not a string")?; + let bytes = local_file(root, rel)?; + let value: Value = + serde_json::from_slice(&bytes).with_context(|| format!("parse {rel}"))?; + schemas.insert( + domain.clone(), + (rel.to_string(), wa_text::sha256_hex(&bytes), value), + ); + } } Ok(Self { manifest, lock, files, + schemas, }) } @@ -263,39 +278,46 @@ pub fn report(old: &Path, new: &Path, evidence: Option<&Path>) -> Result let old = Snapshot::read(old)?; let new = Snapshot::read(new)?; let mut changes = Vec::new(); - for domain in old - .files - .keys() - .chain(new.files.keys()) - .collect::>() - { - let a = old.files.get(domain); - let b = new.files.get(domain); - let deltas = match (a, b) { - (Some((_, _, av)), Some((_, _, bv))) => document_diff(domain, av, bv), - _ => { - let mut d = Vec::new(); - walk(a.map(|v| &v.2), b.map(|v| &v.2), "", "", &mut d); - d + for (is_schema, old_files, new_files) in [ + (false, &old.files, &new.files), + (true, &old.schemas, &new.schemas), + ] { + for domain in old_files + .keys() + .chain(new_files.keys()) + .collect::>() + { + let a = old_files.get(domain); + let b = new_files.get(domain); + let deltas = match (a, b) { + (Some((_, _, av)), Some((_, _, bv))) if !is_schema => document_diff(domain, av, bv), + _ => { + let mut d = Vec::new(); + walk(a.map(|v| &v.2), b.map(|v| &v.2), "", "", &mut d); + d + } + }; + for mut change in deltas { + change["domain"] = json!(domain); + if is_schema { + change["artifactKind"] = json!("schema"); + } + change["oldSource"] = reference(a, &change["oldPath"]); + change["newSource"] = reference(b, &change["newPath"]); + // A missing field still belongs to a concrete domain document. Bind + // that document even though there is no field pointer to reference. + change["oldArtifactSha256"] = json!(a.map(|(_, hash, _)| hash)); + change["newArtifactSha256"] = json!(b.map(|(_, hash, _)| hash)); + // Bind a review to exact artifact contents AND inputs, not just a name + // that can survive an unrelated future snapshot. + change["oldSetHash"] = json!(old.lock.set_hash); + change["newSetHash"] = json!(new.lock.set_hash); + change["id"] = json!(wa_text::sha256_hex( + serde_json::to_string(&change)?.as_bytes() + )); + change["assessment"] = json!({"classification": "indeterminate", "basis": "Contract delta only; inspect pinned sources and same-input regeneration."}); + changes.push(change); } - }; - for mut change in deltas { - change["domain"] = json!(domain); - change["oldSource"] = reference(a, &change["oldPath"]); - change["newSource"] = reference(b, &change["newPath"]); - // A missing field still belongs to a concrete domain document. Bind - // that document even though there is no field pointer to reference. - change["oldArtifactSha256"] = json!(a.map(|(_, hash, _)| hash)); - change["newArtifactSha256"] = json!(b.map(|(_, hash, _)| hash)); - // Bind a review to exact artifact contents AND inputs, not just a name - // that can survive an unrelated future snapshot. - change["oldSetHash"] = json!(old.lock.set_hash); - change["newSetHash"] = json!(new.lock.set_hash); - change["id"] = json!(wa_text::sha256_hex( - serde_json::to_string(&change)?.as_bytes() - )); - change["assessment"] = json!({"classification": "indeterminate", "basis": "Contract delta only; inspect pinned sources and same-input regeneration."}); - changes.push(change); } } if let Some(path) = evidence { @@ -471,6 +493,35 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } + #[test] + fn schema_only_changes_are_reported_and_missing_declared_schemas_fail() { + let dir = + std::env::temp_dir().join(format!("whatspec-schema-delta-{}", std::process::id())); + let write = |name: &str, schema: Value| { + let root = dir.join(name); + std::fs::create_dir_all(&root).unwrap(); + let lock = wa_store::lock::BundleLock::new("test", vec![]); + std::fs::write(root.join("bundles.lock.json"), lock.to_pretty_json()).unwrap(); + let doc = json!({"waVersion":"test", "schemaVersion":"4.3.0"}).to_string(); + std::fs::write(root.join("index.json"), &doc).unwrap(); + let manifest = json!({"waVersion":"test", "schemaVersion":"4.3.0", "domains":{"test":{"file":"index.json", "schema":"schema.json", "sha256":wa_text::sha256_hex(doc.as_bytes())}}}); + std::fs::write(root.join("manifest.json"), manifest.to_string()).unwrap(); + std::fs::write(root.join("schema.json"), schema.to_string()).unwrap(); + root + }; + let old = write("old", json!({"type":"object", "required":[]})); + let new = write("new", json!({"type":"object", "required":["added"]})); + let report = report(&old, &new, None).unwrap(); + assert_eq!(report["changes"].as_array().unwrap().len(), 1); + let delta = &report["changes"][0]; + assert_eq!(delta["oldSource"]["file"], "schema.json"); + assert_eq!(delta["oldPath"], "/required"); + assert_ne!(delta["oldArtifactSha256"], delta["newArtifactSha256"]); + std::fs::remove_file(new.join("schema.json")).unwrap(); + assert!(Snapshot::read(&new).is_err()); + std::fs::remove_dir_all(dir).unwrap(); + } + #[test] fn reviewed_causes_are_explicit_and_cannot_be_renames() { let mut changes = [json!({"id":"current"})]; From 106c7621348935daa343edf73ebc6ad50ad798a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:05:35 +0000 Subject: [PATCH 27/52] fix: record provenance selection scope and review evidence --- crates/whatspec/src/source_index.rs | 10 +- docs/snapshot-quality-2026-10-07.md | 37 +- docs/snapshot-quality-2026-10-07.sources.json | 494 +++++++++++++++++- docs/snapshot-quality.md | 10 +- 4 files changed, 539 insertions(+), 12 deletions(-) diff --git a/crates/whatspec/src/source_index.rs b/crates/whatspec/src/source_index.rs index f8cb2a22..dd1cec5a 100644 --- a/crates/whatspec/src/source_index.rs +++ b/crates/whatspec/src/source_index.rs @@ -61,9 +61,11 @@ pub fn index(lock_path: &Path, bundles: &Path, selected: &BTreeSet<&str>) -> Res json!({"sourceIndexVersion":1,"waVersion":lock.wa_version,"setHash":lock.set_hash, "bundleCount":lock.bundle_count,"modules":definitions, "coverage":"parsed-static-definitions", + "selection": if selected.is_empty() { json!({"mode":"all"}) } else { json!({"mode":"selected","modules":selected}) }, "limits":["Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", "Every bundle parsed without recovery. Absence only establishes that this exact static module name was not defined in these inputs; it does not prove feature removal or a rename.", - "All recovered occurrences are retained. Equal module names with different hashes need review.", + "When selection.mode is selected, absence conclusions apply only to names in selection.modules. Other names were not requested.", + "All recovered occurrences of selected names are retained. Equal module names with different hashes need review.", "Module provenance locates code; field-level semantics may require following dependencies."]}), ) } @@ -104,6 +106,12 @@ mod tests { assert!(index(&lock_path, &dir, &BTreeSet::new()).is_err()); std::fs::write(dir.join("one.js"), bytes).unwrap(); assert!(index(&lock_path, &dir, &BTreeSet::new()).is_ok()); + let selected = index(&lock_path, &dir, &BTreeSet::from(["Missing"])).unwrap(); + assert_eq!( + selected["selection"], + json!({"mode":"selected", "modules":["Missing"]}) + ); + assert!(selected["modules"].as_object().unwrap().is_empty()); std::fs::write(dir.join("duplicate.js"), bytes).unwrap(); assert!(index(&lock_path, &dir, &BTreeSet::new()).is_err()); std::fs::remove_file(dir.join("duplicate.js")).unwrap(); diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index bde5f0ca..8e09f0f9 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -131,17 +131,35 @@ local-object fix. A smaller synthetic refactor also fails before and passes afte Recovery accepts only a uniquely bound, directly initialized function-local object, with all uses accounted for as recognized constructor reads or sources of a fresh-target `babelHelpers.extends`. Mutation, escape, rebinding, conditional -initialization, pre-initialization reads, direct eval, shadowing and outer closure +initialization, pre-initialization reads, direct eval (including parenthesized +callees), shadowing and outer closure reads remain unresolved. The pinned `WAWebWamTypeHash` constructor copies input fields through `set`; it does not mutate the source object. The async closure's inherited fields remain partial because invocation timing is not established. -The fix recovers 14 unread arguments on the current set and 14 on the old set +The initial local-object fix (`58afb2e`) recovers 14 unread arguments on the current set and 14 on the old set (the memberships differ; the ledger lists both). On the current snapshot it adds 157 field occurrences, six field values, and two distinct call sites; partial sites fall from 126 to 114. Remaining unread arguments are 75 identifiers, one call and three members. Only `generated/wam/index.json` and `generated/manifest.json` change. No IQ, `spec.rs`, or IR schema redesign occurs. +A review follow-up also recovers approved locals used inside fresh-target merges. +`WAWebKeepInChatMsgAction` supplies nine fields through its local object in each +of two constructors. `WAWebSmbMarkAsXOrderAction` supplies seven in each of two +constructors; their additional call operands remain unread, so those sites stay +partial. The follow-up adds 32 field occurrences and eight values, and separates +two previously identical partial sites. Totals become 813 sites, 3,478 field +occurrences, 804 values and 113 partial sites. All six baseline pins remain +unchanged. Both source modules are in the focused sidecar. Regression tests +reproduce the omitted merge fields and preserve partial status for unknown +operands, escaped locals, mutated targets and outer closure reads. + +The nested-definition provenance review separately exposed overlapping WAM +scans. Its regression counted each gap twice before the fix. `quality-gaps` now +uses the normal extractor's outer module selection, while the source index +continues to include nested definitions. Neither locked snapshot contains such +nested definitions, so their reviewed gap ledgers are unchanged. + Existing extraction entry points and `WamDiagnostics` retain their API; the gap sidecar uses a new optional function. IR schema stays 4.3.0 because this fills existing field semantics and makes no consumer contract change. @@ -164,11 +182,11 @@ cargo test -p wa-transform -p wa-wam -p whatspec --all-features ``` Source indexes are generated on demand (~14.2 MB each), not committed wholesale. -The focused source and gap ledgers are ~66.6 KB and ~21.4 KB. Two real WAM fixtures +The focused source and gap ledgers are ~86.5 KB and ~21.4 KB. Two real WAM fixtures retain exact source slices, with bundle hashes and offsets in their headers. -The WAM document grows from 2,794,121 to 2,812,306 bytes by recovering existing +The WAM document grows from 2,794,121 to 2,816,845 bytes by recovering existing field semantics; optional provenance adds no per-field IR metadata. WAM gap evidence incurs extra parsing only -when requested. Final regeneration checks reproduce all 27 artifacts for the repaired current +when requested. At the initial local-object revision, regeneration checks reproduce all 27 artifacts for the repaired current snapshot and a separately regenerated old snapshot. Observed check times were 26.063 s (new) and 18.561 s (old); peak child RSS across the sequence was 2,390,792 KiB. Compilation overlapped these runs, so they are cost observations, @@ -177,9 +195,14 @@ not an isolated performance comparison. Changes to snapshot membership require reviewing these ledgers; count arithmetic alone is insufficient. -The generic contract report contains 484 deltas (2,637,575 bytes), keeps ordered +Unknown catalog-field writes also receive one gap location per counted +occurrence, with the field name, rather than a counter without source evidence. +Filtered source sidecars now explicitly record their requested module names; +absence findings are limited to that selection. + +The generic contract report contains 486 deltas (2,652,610 bytes), keeps ordered arrays and does not infer renames. The [review ledger](snapshot-quality-2026-10-07.reviews.json) classifies the privacy -improvement and persisted-ID change; the remaining 482 deltas stay indeterminate until +improvement and persisted-ID change; the remaining 484 deltas stay indeterminate until reviewed. `--evidence` binds each review to both artifact hashes. The baseline dispositions above concern those six counters, not all contract differences. diff --git a/docs/snapshot-quality-2026-10-07.sources.json b/docs/snapshot-quality-2026-10-07.sources.json index 7ba66536..9c6f05ea 100644 --- a/docs/snapshot-quality-2026-10-07.sources.json +++ b/docs/snapshot-quality-2026-10-07.sources.json @@ -5,7 +5,8 @@ "limits": [ "Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", "Every bundle parsed without recovery. Absence only establishes that this exact static module name was not defined in these inputs; it does not prove feature removal or a rename.", - "All recovered occurrences are retained. Equal module names with different hashes need review.", + "When selection.mode is selected, absence conclusions apply only to names in selection.modules. Other names were not requested.", + "All recovered occurrences of selected names are retained. Equal module names with different hashes need review.", "Module provenance locates code; field-level semantics may require following dependencies." ], "modules": { @@ -191,6 +192,150 @@ "start": 500906 } ], + "WAWebKeepInChatMsgAction": [ + { + "bundleSha256": "034314e49fa571a8e2e8491843a15536326b4a47d1f66ac7704d2332ee1b391e", + "dependencies": [ + "fbt", + "Promise", + "WAJobOrchestratorTypes", + "WALogger", + "WATimeUtils", + "WAWebAck", + "WAWebAssociationProcessor", + "WAWebAssociationProcessorConstants", + "WAWebChatCollection", + "WAWebChatEphemerality", + "WAWebChatGetters", + "WAWebChatSendMessages", + "WAWebChatThreadLogging", + "WAWebDBDeleteAssociatedMsgsByMsgKey", + "WAWebDBMarkFutureproofMessagesReparsed", + "WAWebDBMsgUtils", + "WAWebDBProcessMessage", + "WAWebDBStoreMessageOrphans", + "WAWebDBUpdateMessageTable", + "WAWebDisappearingMessageKeepInChatWamEvent", + "WAWebEphemeralConstants", + "WAWebEphemeralKeepInChatUtils", + "WAWebEphemeralKeepInChatWamUtils", + "WAWebFrontendMsgGetters", + "WAWebKeepInChatMsgUtils", + "WAWebKeepInChatPerfWamEvent", + "WAWebKicNux.react", + "WAWebLidMigrationUtils", + "WAWebMessageAssociationGatingUtils", + "WAWebMessageSendPerfReporter", + "WAWebMessageSendReporter", + "WAWebMessageSendReporterFrontendDeps", + "WAWebMsgCollection", + "WAWebMsgGetters", + "WAWebMsgInfoUtils", + "WAWebMsgKey", + "WAWebMsgModel", + "WAWebMsgModelFromData", + "WAWebMsgModelUtils", + "WAWebMsgType", + "WAWebNotificationController", + "WAWebNotificationsKeepInChatNotification", + "WAWebOrchestratorNonPersistedJob", + "WAWebProtobufsE2E.pb", + "WAWebSendMsgRecordAction", + "WAWebToast.react", + "WAWebToastManager", + "WAWebUserPrefsMeUser", + "WAWebViewMode.flow", + "WAWebWamEnumKicActionNameType", + "WAWebWamEnumKicActorType", + "WAWebWamEnumKicErrorCodeType", + "WAWebWamEnumKicRequestTypeType", + "WAWebWamEnumResponseType", + "WAWebWamEnumTriggerType", + "WAWebWamMsgUtils", + "WAWebWid", + "WAWebWidFactory", + "asyncToGeneratorRuntime", + "getErrorSafe", + "nullthrows", + "react" + ], + "end": 4411371, + "factorySha256": "c620e85d7015f97121b741081e8a1f5857f0d83694530678efdf6927f76e5065", + "moduleSha256": "e1638ff1eeb35e1030f8fa4c51c64d1994691ace5a22dca36657f06a6c6d4774", + "start": 4397855 + }, + { + "bundleSha256": "6cc9081de20b92d22decaeadc2b261f91038ca38434b6e4796cd038f1836ede7", + "dependencies": [ + "fbt", + "Promise", + "WAJobOrchestratorTypes", + "WALogger", + "WATimeUtils", + "WAWebAck", + "WAWebAssociationProcessor", + "WAWebAssociationProcessorConstants", + "WAWebChatCollection", + "WAWebChatEphemerality", + "WAWebChatGetters", + "WAWebChatSendMessages", + "WAWebChatThreadLogging", + "WAWebDBDeleteAssociatedMsgsByMsgKey", + "WAWebDBMarkFutureproofMessagesReparsed", + "WAWebDBMsgUtils", + "WAWebDBProcessMessage", + "WAWebDBStoreMessageOrphans", + "WAWebDBUpdateMessageTable", + "WAWebDisappearingMessageKeepInChatWamEvent", + "WAWebEphemeralConstants", + "WAWebEphemeralKeepInChatUtils", + "WAWebEphemeralKeepInChatWamUtils", + "WAWebFrontendMsgGetters", + "WAWebKeepInChatMsgUtils", + "WAWebKeepInChatPerfWamEvent", + "WAWebKicNux.react", + "WAWebLidMigrationUtils", + "WAWebMessageAssociationGatingUtils", + "WAWebMessageSendPerfReporter", + "WAWebMessageSendReporter", + "WAWebMessageSendReporterFrontendDeps", + "WAWebMsgCollection", + "WAWebMsgGetters", + "WAWebMsgInfoUtils", + "WAWebMsgKey", + "WAWebMsgModel", + "WAWebMsgModelFromData", + "WAWebMsgModelUtils", + "WAWebMsgType", + "WAWebNotificationController", + "WAWebNotificationsKeepInChatNotification", + "WAWebOrchestratorNonPersistedJob", + "WAWebProtobufsE2E.pb", + "WAWebSendMsgRecordAction", + "WAWebToast.react", + "WAWebToastManager", + "WAWebUserPrefsMeUser", + "WAWebViewMode.flow", + "WAWebWamEnumKicActionNameType", + "WAWebWamEnumKicActorType", + "WAWebWamEnumKicErrorCodeType", + "WAWebWamEnumKicRequestTypeType", + "WAWebWamEnumResponseType", + "WAWebWamEnumTriggerType", + "WAWebWamMsgUtils", + "WAWebWid", + "WAWebWidFactory", + "asyncToGeneratorRuntime", + "getErrorSafe", + "nullthrows", + "react" + ], + "end": 232340, + "factorySha256": "c620e85d7015f97121b741081e8a1f5857f0d83694530678efdf6927f76e5065", + "moduleSha256": "e1638ff1eeb35e1030f8fa4c51c64d1994691ace5a22dca36657f06a6c6d4774", + "start": 218824 + } + ], "WAWebKmpSyncdRequestBuilder": [ { "bundleSha256": "ffe69f61924eb23c1d65cb89a152e4ce35ee404bd52a7edcced9c037b236f490", @@ -379,6 +524,30 @@ "start": 4130648 } ], + "WAWebSmbMarkAsXOrderAction": [ + { + "bundleSha256": "469e8e15743d3d0d694e5157379c9d8e7b55ab57a790d1b129112021619f7e02", + "dependencies": [ + "WASmaxInBizSettingsEnums", + "WATimeUtils", + "WAWebCTWADataSharingModel", + "WAWebCTWAGatingUtils", + "WAWebChatThreadLogging", + "WAWebCommonCTWADataSharing", + "WAWebConversionTupleCollection", + "WAWebConversionTupleModel", + "WAWebCtwaOrderSignalWamEvent", + "WAWebOrderStatus", + "WAWebPerCustomerDataSharingUtils", + "WAWebWamEnumOrderSignalType", + "WAWebWamEnumOrderStatus" + ], + "end": 685251, + "factorySha256": "125d3d53c2827ca3d567e91463710aadb4a4d0ef51b4da9b151b57456e22979f", + "moduleSha256": "deab3df6b2d904ea080a98abbf83c04f25c1d949e03d72ca20904f67291d4055", + "start": 681256 + } + ], "WAWebStatusSubtitle.react": [ { "bundleSha256": "72622fd528bcdb8719660018f754e37ce201276a4c5fdead04f82bc81648de8d", @@ -1070,6 +1239,83 @@ } ] }, + "selection": { + "mode": "selected", + "modules": [ + "EBMessageMetadataQuery", + "EBMessageMetadataQueryQuery.graphql", + "EBMessageMetadataQueryQuery_facebookRelayOperation", + "WAWebAccountLinkingAPIGetCertsQuery.graphql", + "WAWebContactManagerCustomerProfilesQuery", + "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation", + "WAWebCreateLabyrinthBackupJob", + "WAWebCreateLabyrinthBackupJobMutation.graphql", + "WAWebDebugLabyrinthInboxSnapshotQuery", + "WAWebDebugLabyrinthInboxSnapshotQuery.graphql", + "WAWebDebugLabyrinthRangeQuery", + "WAWebDebugLabyrinthRangeQuery.graphql", + "WAWebFilterLogging", + "WAWebKeepInChatMsgAction", + "WAWebKmpSyncdRequestBuilder", + "WAWebMexLogging", + "WAWebMexUpdateNewsletterAdminProfileSettingJob", + "WAWebMexUpdateNewsletterAdminProfileSettingJobMutation.graphql", + "WAWebPaymentsUserActionLogging", + "WAWebPixWamLogger", + "WAWebRotateLabyrinthEpochJob", + "WAWebRotateLabyrinthEpochJobMutation.graphql", + "WAWebSendStickerAction", + "WAWebSetAboutJob", + "WAWebSetPrivacyJob", + "WAWebSmb3pdAggregatedConversionSignalAction", + "WAWebSmbMarkAsXOrderAction", + "WAWebStatusSubtitle.react", + "WAWebStatusViewerActionWamEvent", + "WAWebTeamLinkCreateInvitationDialog.react", + "WAWebTeamLinkCreateInvitationMutation", + "WAWebTeamLinkCreateInvitationMutation.graphql", + "WAWebTeamLinkCreateInvitationMutation_facebookRelayOperation", + "WAWebTeamLinkListInvitationsQuery", + "WAWebTeamLinkListInvitationsQuery.graphql", + "WAWebTeamLinkListInvitationsQuery_facebookRelayOperation", + "WAWebTeamLinkRemoveInvitationMutation", + "WAWebTeamLinkRemoveInvitationMutation.graphql", + "WAWebTeamLinkRemoveInvitationMutation_facebookRelayOperation", + "WAWebThreadLoggingAi", + "WAWebThreadLoggingBiz", + "WAWebThreadLoggingCoreConsumer", + "WAWebThreadLoggingIntegrity", + "WAWebThreadLoggingNotification", + "WAWebThreadLoggingVoip", + "WAWebUpdateNewsletterAdminProfileSettingJob", + "WAWebUploadLabyrinthMessagesJob", + "WAWebUploadLabyrinthMessagesJobMutation.graphql", + "WAWebWamCodegenUtils", + "WAWebWamFingerprintReporter", + "WAWebWamTypeHash", + "WAWebWindowsHybridBridgeWam.v2589", + "WAWebWindowsHybridBridgeWam.v2590", + "WAWebWindowsHybridBridgeWam.v2591", + "WAWebWindowsHybridBridgeWam.v2601", + "WAWebWindowsHybridBridgeWam.v2602", + "WAWebWindowsHybridBridgeWam.v2603", + "WAWebWindowsHybridBridgeWam.v2604", + "WAWebWindowsHybridBridgeWam.v2608", + "WAWebWindowsHybridBridgeWam.v2609", + "WAWebWindowsHybridBridgeWam.v2610", + "WAWebWindowsHybridBridgeWam.v2617", + "WAWebWindowsHybridBridgeWam.v2618", + "WAWebWindowsHybridBridgeWam.v2619", + "WAWebWindowsHybridBridgeWam.v2621", + "WAWebWindowsHybridBridgeWam.v2624", + "WAWebWindowsHybridBridgeWam.v2634", + "WAWebWindowsHybridBridgeWam.v2635", + "WAWebWindowsHybridBridgeWam.v2636", + "useWAWebSmartComposerCoachSuggestedReply", + "useWAWebSmartComposerCoachSuggestedReplyMutation.graphql", + "useWAWebSmartComposerCoachSuggestedReplyMutation_facebookRelayOperation" + ] + }, "setHash": "99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00", "sourceIndexVersion": 1, "waVersion": "2.3000.1045368834" @@ -1080,7 +1326,8 @@ "limits": [ "Offsets are UTF-8 byte offsets, end exclusive, in the bundle identified by SHA-256.", "Every bundle parsed without recovery. Absence only establishes that this exact static module name was not defined in these inputs; it does not prove feature removal or a rename.", - "All recovered occurrences are retained. Equal module names with different hashes need review.", + "When selection.mode is selected, absence conclusions apply only to names in selection.modules. Other names were not requested.", + "All recovered occurrences of selected names are retained. Equal module names with different hashes need review.", "Module provenance locates code; field-level semantics may require following dependencies." ], "modules": { @@ -1154,6 +1401,148 @@ "start": 399753 } ], + "WAWebKeepInChatMsgAction": [ + { + "bundleSha256": "9b681e073f14f9d50e3dc12b456e4f980b554eff4d58827bc91c90a697e9ca38", + "dependencies": [ + "fbt", + "Promise", + "WAJobOrchestratorTypes", + "WALogger", + "WATimeUtils", + "WAWebAck", + "WAWebAssociationProcessor", + "WAWebAssociationProcessorConstants", + "WAWebChatCollection", + "WAWebChatEphemerality", + "WAWebChatGetters", + "WAWebChatSendMessages", + "WAWebChatThreadLogging", + "WAWebDBDeleteAssociatedMsgsByMsgKey", + "WAWebDBMarkFutureproofMessagesReparsed", + "WAWebDBMsgUtils", + "WAWebDBProcessMessage", + "WAWebDBStoreMessageOrphans", + "WAWebDBUpdateMessageTable", + "WAWebDisappearingMessageKeepInChatWamEvent", + "WAWebEphemeralConstants", + "WAWebEphemeralKeepInChatUtils", + "WAWebEphemeralKeepInChatWamUtils", + "WAWebFrontendMsgGetters", + "WAWebKeepInChatMsgUtils", + "WAWebKeepInChatPerfWamEvent", + "WAWebKicNux.react", + "WAWebLidMigrationUtils", + "WAWebMessageSendPerfReporter", + "WAWebMessageSendReporter", + "WAWebMessageSendReporterFrontendDeps", + "WAWebMsgCollection", + "WAWebMsgGetters", + "WAWebMsgInfoUtils", + "WAWebMsgKey", + "WAWebMsgModel", + "WAWebMsgModelFromData", + "WAWebMsgModelUtils", + "WAWebMsgType", + "WAWebNotificationController", + "WAWebNotificationsKeepInChatNotification", + "WAWebOrchestratorNonPersistedJob", + "WAWebProtobufsE2E.pb", + "WAWebSendMsgRecordAction", + "WAWebToast.react", + "WAWebToastManager", + "WAWebUserPrefsMeUser", + "WAWebViewMode.flow", + "WAWebWamEnumKicActionNameType", + "WAWebWamEnumKicActorType", + "WAWebWamEnumKicErrorCodeType", + "WAWebWamEnumKicRequestTypeType", + "WAWebWamEnumResponseType", + "WAWebWamEnumTriggerType", + "WAWebWamMsgUtils", + "WAWebWid", + "WAWebWidFactory", + "asyncToGeneratorRuntime", + "getErrorSafe", + "nullthrows", + "react" + ], + "end": 2755620, + "factorySha256": "43bf65697357cf360b09a48c02cdb9c5f5bfbe094b98d9cb76fd01e0b9377f0f", + "moduleSha256": "c0b42bc884701648422742b7997587507355f5e368281cbaee62f54b69069468", + "start": 2742293 + }, + { + "bundleSha256": "a780a7bd5608c868c10f5245365612e6a2e037c4097f25e71dafdf3a6ce94d25", + "dependencies": [ + "fbt", + "Promise", + "WAJobOrchestratorTypes", + "WALogger", + "WATimeUtils", + "WAWebAck", + "WAWebAssociationProcessor", + "WAWebAssociationProcessorConstants", + "WAWebChatCollection", + "WAWebChatEphemerality", + "WAWebChatGetters", + "WAWebChatSendMessages", + "WAWebChatThreadLogging", + "WAWebDBDeleteAssociatedMsgsByMsgKey", + "WAWebDBMarkFutureproofMessagesReparsed", + "WAWebDBMsgUtils", + "WAWebDBProcessMessage", + "WAWebDBStoreMessageOrphans", + "WAWebDBUpdateMessageTable", + "WAWebDisappearingMessageKeepInChatWamEvent", + "WAWebEphemeralConstants", + "WAWebEphemeralKeepInChatUtils", + "WAWebEphemeralKeepInChatWamUtils", + "WAWebFrontendMsgGetters", + "WAWebKeepInChatMsgUtils", + "WAWebKeepInChatPerfWamEvent", + "WAWebKicNux.react", + "WAWebLidMigrationUtils", + "WAWebMessageSendPerfReporter", + "WAWebMessageSendReporter", + "WAWebMessageSendReporterFrontendDeps", + "WAWebMsgCollection", + "WAWebMsgGetters", + "WAWebMsgInfoUtils", + "WAWebMsgKey", + "WAWebMsgModel", + "WAWebMsgModelFromData", + "WAWebMsgModelUtils", + "WAWebMsgType", + "WAWebNotificationController", + "WAWebNotificationsKeepInChatNotification", + "WAWebOrchestratorNonPersistedJob", + "WAWebProtobufsE2E.pb", + "WAWebSendMsgRecordAction", + "WAWebToast.react", + "WAWebToastManager", + "WAWebUserPrefsMeUser", + "WAWebViewMode.flow", + "WAWebWamEnumKicActionNameType", + "WAWebWamEnumKicActorType", + "WAWebWamEnumKicErrorCodeType", + "WAWebWamEnumKicRequestTypeType", + "WAWebWamEnumResponseType", + "WAWebWamEnumTriggerType", + "WAWebWamMsgUtils", + "WAWebWid", + "WAWebWidFactory", + "asyncToGeneratorRuntime", + "getErrorSafe", + "nullthrows", + "react" + ], + "end": 224710, + "factorySha256": "43bf65697357cf360b09a48c02cdb9c5f5bfbe094b98d9cb76fd01e0b9377f0f", + "moduleSha256": "c0b42bc884701648422742b7997587507355f5e368281cbaee62f54b69069468", + "start": 211383 + } + ], "WAWebMexLogging": [ { "bundleSha256": "430cb8979511aada95178605faa35a8483446f677ce6d1fe0fd66333c7879eee", @@ -1332,6 +1721,30 @@ "start": 2527583 } ], + "WAWebSmbMarkAsXOrderAction": [ + { + "bundleSha256": "7c7be0378ce2e7ff49b9f5909cf8a7d283000af82cc43bc071c2df3541ec5d56", + "dependencies": [ + "WASmaxInBizSettingsEnums", + "WATimeUtils", + "WAWebCTWADataSharingModel", + "WAWebCTWAGatingUtils", + "WAWebChatThreadLogging", + "WAWebCommonCTWADataSharing", + "WAWebConversionTupleCollection", + "WAWebConversionTupleModel", + "WAWebCtwaOrderSignalWamEvent", + "WAWebOrderStatus", + "WAWebPerCustomerDataSharingUtils", + "WAWebWamEnumOrderSignalType", + "WAWebWamEnumOrderStatus" + ], + "end": 924764, + "factorySha256": "125d3d53c2827ca3d567e91463710aadb4a4d0ef51b4da9b151b57456e22979f", + "moduleSha256": "deab3df6b2d904ea080a98abbf83c04f25c1d949e03d72ca20904f67291d4055", + "start": 920769 + } + ], "WAWebStatusSubtitle.react": [ { "bundleSha256": "7a224a1f4e67acb7e519b9676abfa86806e50426e7ed4a9f76e517536fe279ca", @@ -1679,6 +2092,83 @@ } ] }, + "selection": { + "mode": "selected", + "modules": [ + "EBMessageMetadataQuery", + "EBMessageMetadataQueryQuery.graphql", + "EBMessageMetadataQueryQuery_facebookRelayOperation", + "WAWebAccountLinkingAPIGetCertsQuery.graphql", + "WAWebContactManagerCustomerProfilesQuery", + "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation", + "WAWebCreateLabyrinthBackupJob", + "WAWebCreateLabyrinthBackupJobMutation.graphql", + "WAWebDebugLabyrinthInboxSnapshotQuery", + "WAWebDebugLabyrinthInboxSnapshotQuery.graphql", + "WAWebDebugLabyrinthRangeQuery", + "WAWebDebugLabyrinthRangeQuery.graphql", + "WAWebFilterLogging", + "WAWebKeepInChatMsgAction", + "WAWebKmpSyncdRequestBuilder", + "WAWebMexLogging", + "WAWebMexUpdateNewsletterAdminProfileSettingJob", + "WAWebMexUpdateNewsletterAdminProfileSettingJobMutation.graphql", + "WAWebPaymentsUserActionLogging", + "WAWebPixWamLogger", + "WAWebRotateLabyrinthEpochJob", + "WAWebRotateLabyrinthEpochJobMutation.graphql", + "WAWebSendStickerAction", + "WAWebSetAboutJob", + "WAWebSetPrivacyJob", + "WAWebSmb3pdAggregatedConversionSignalAction", + "WAWebSmbMarkAsXOrderAction", + "WAWebStatusSubtitle.react", + "WAWebStatusViewerActionWamEvent", + "WAWebTeamLinkCreateInvitationDialog.react", + "WAWebTeamLinkCreateInvitationMutation", + "WAWebTeamLinkCreateInvitationMutation.graphql", + "WAWebTeamLinkCreateInvitationMutation_facebookRelayOperation", + "WAWebTeamLinkListInvitationsQuery", + "WAWebTeamLinkListInvitationsQuery.graphql", + "WAWebTeamLinkListInvitationsQuery_facebookRelayOperation", + "WAWebTeamLinkRemoveInvitationMutation", + "WAWebTeamLinkRemoveInvitationMutation.graphql", + "WAWebTeamLinkRemoveInvitationMutation_facebookRelayOperation", + "WAWebThreadLoggingAi", + "WAWebThreadLoggingBiz", + "WAWebThreadLoggingCoreConsumer", + "WAWebThreadLoggingIntegrity", + "WAWebThreadLoggingNotification", + "WAWebThreadLoggingVoip", + "WAWebUpdateNewsletterAdminProfileSettingJob", + "WAWebUploadLabyrinthMessagesJob", + "WAWebUploadLabyrinthMessagesJobMutation.graphql", + "WAWebWamCodegenUtils", + "WAWebWamFingerprintReporter", + "WAWebWamTypeHash", + "WAWebWindowsHybridBridgeWam.v2589", + "WAWebWindowsHybridBridgeWam.v2590", + "WAWebWindowsHybridBridgeWam.v2591", + "WAWebWindowsHybridBridgeWam.v2601", + "WAWebWindowsHybridBridgeWam.v2602", + "WAWebWindowsHybridBridgeWam.v2603", + "WAWebWindowsHybridBridgeWam.v2604", + "WAWebWindowsHybridBridgeWam.v2608", + "WAWebWindowsHybridBridgeWam.v2609", + "WAWebWindowsHybridBridgeWam.v2610", + "WAWebWindowsHybridBridgeWam.v2617", + "WAWebWindowsHybridBridgeWam.v2618", + "WAWebWindowsHybridBridgeWam.v2619", + "WAWebWindowsHybridBridgeWam.v2621", + "WAWebWindowsHybridBridgeWam.v2624", + "WAWebWindowsHybridBridgeWam.v2634", + "WAWebWindowsHybridBridgeWam.v2635", + "WAWebWindowsHybridBridgeWam.v2636", + "useWAWebSmartComposerCoachSuggestedReply", + "useWAWebSmartComposerCoachSuggestedReplyMutation.graphql", + "useWAWebSmartComposerCoachSuggestedReplyMutation_facebookRelayOperation" + ] + }, "setHash": "05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b", "sourceIndexVersion": 1, "waVersion": "2.3000.1047483476" diff --git a/docs/snapshot-quality.md b/docs/snapshot-quality.md index 29019007..6bf4ad9c 100644 --- a/docs/snapshot-quality.md +++ b/docs/snapshot-quality.md @@ -3,6 +3,9 @@ The existing text `whatspec diff OLD NEW` is a count and name summary. Use `whatspec diff OLD NEW --json` for contract deltas across every manifest domain. The report checks domain hashes, document versions, and lock self-consistency. +It also loads every declared schema and reports schema-only changes with +`artifactKind: schema`, separate content hashes, and JSON Pointers. Schema hashes +are computed from the snapshot files; the manifest currently does not pin them. It fails on missing declared artifacts. Schema validation preflights every schema-valued location and referenced target against an in-memory registry, including unused definitions and absent optional properties. Local pointers, @@ -56,7 +59,9 @@ whatspec source-index generated/bundles.lock.json bundles WAWebSetPrivacyJob > s with a checked AST entry point and emits a separate optional sidecar containing bundle SHA-256, module and factory hashes, dependency names, and byte offsets. It never evaluates JavaScript. Source names may be selected after the two paths; -omit them to index all recovered modules. Offsets are UTF-8 bytes with an +omit them to index all recovered modules. `selection.mode` records `all` or +`selected`; the latter also records every requested name, including missing ones. +An absent name outside that selection is not evidence of source absence. Offsets are UTF-8 bytes with an exclusive end, so invalid UTF-8 is rejected rather than decoded lossily. Every recovered occurrence remains visible, including conflicting definitions. The index is deterministic and contains no workspace-specific file paths. @@ -72,7 +77,8 @@ For a field-level review, cite both its IR pointer and the relevant module span. This keeps recoverable provenance outside every repeated field of the IR. `whatspec quality-gaps LOCK BUNDLE_DIR` emits counted WAM gap locations with -module-relative constructor byte spans, constructor/module hashes, and matching +module-relative constructor byte spans, constructor/module hashes, optional +unknown-field names, and matching bundle locations. It checks the same complete bundle set and parse coverage. Two reasons may point at one construction; repeated copies of a module follow the extractor's existing deduplication. Counted diagnostics use the same outer From 79da4d9ebccd40f6566bd4d5e4a603cf72de3cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:13:13 +0000 Subject: [PATCH 28/52] test: record independent quality follow-up validation and remaining gap --- tests/conformance/compiled/README.md | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index 7d7bec85..09b376ab 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -243,3 +243,39 @@ those four targets and passes after the rename. `cargo build --workspace --examp --message-format=json` reports four distinct executable paths. Parallel `cargo test --workspace` passes 1,657 tests with zero failures/ignored tests and no output-collision warning. Parallelism was not reduced. + +## Quality follow-up 106c762 + +Integrated #52 at `106c7621348935daa343edf73ebc6ad50ad798a8` with +ancestry intact and no conflicts. Its nested-module, local-merge, unknown-field +site and schema-diff regressions pass. The workspace now passes 1,663 tests, +including the compiled pilot suites, with zero failures or ignored tests. +All-feature/all-target Clippy, formatting, 12/12 committed schemas, exact lint, +15 quality Python tests and five capture tests pass. + +Review dispositions remain separate: + +- PRRT_kwDOSxmno86qIUij: the nested-definition regression establishes the same + counted definitions and diagnostic totals as production extraction. The full + nested source index is retained for provenance lookup. +- PRRT_kwDOSxmno86qIUin: unknown-field sites are fixed, but global-channel gaps + remain unlocated. The independently built CLI reproduces a counter of two with + an empty site list and no explicit unlocated-counter field. Keep this open. +- PRRT_kwDOSxmno86qIUie: manifest-to-lock identity is still owned by quality and + remains open; a matching version alone does not bind the input bundle set. + +The remaining global-channel reproduction uses a synthetic single-bundle lock, +verified by the normal CLI path, without executing JavaScript: + +```javascript +__d("WAWebWamGlobals",["WAWebWamCodegenUtils"],function(t,n,r,o,a,i,l){var e=o("WAWebWamCodegenUtils");l.Global=e.defineGlobal({computed:[3,e.TYPES.STRING,[CHANNEL]],empty:[4,e.TYPES.STRING,[]]})}); +``` + +Using exactly those bytes without a trailing newline yields setHash +`840dfc3f996afcfac451f80043580175b4443db0487ec4c056ffb02bc9fd57be`. +`whatspec quality-gaps LOCK BUNDLE_DIR` returns +`dropsByReason: {"global with an unreadable channel list": 2}` and +`wamGapSites: []`. The review thread records this evidence for the quality owner; +no implementation was duplicated in this workstream. + +The integrated CLI also passes the locked current-snapshot `--check`, reproducing all 27 committed artifacts after the WAM changes. From 04200c9a00784855f29a17f93364c10589632dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:19:26 +0000 Subject: [PATCH 29/52] fix: separate WAM counters without source locations --- crates/whatspec/src/quality_gaps.rs | 67 ++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/whatspec/src/quality_gaps.rs b/crates/whatspec/src/quality_gaps.rs index 08abad27..16775a7b 100644 --- a/crates/whatspec/src/quality_gaps.rs +++ b/crates/whatspec/src/quality_gaps.rs @@ -1,5 +1,5 @@ //! Optional WAM gap evidence tied to a verified bundle set. -use anyhow::{Result, ensure}; +use anyhow::{Context, Result, ensure}; use serde_json::json; use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; @@ -44,11 +44,12 @@ pub fn run(args: &[String]) -> Result<()> { "start":gap.start,"end":gap.end,"eventModule":gap.event_module,"eventExport":gap.event_export,"reason":gap.reason,"field":gap.field, "constructionSha256":wa_text::sha256_hex(&module.as_bytes()[gap.start as usize..gap.end as usize])})); } + let unlocated = unlocated_counts(&diag.drops_by_reason, &sites)?; sites.sort_by_key(|v| serde_json::to_string(v).unwrap()); println!( "{}", serde_json::to_string_pretty( - &json!({"qualityReportVersion":1,"waVersion":lock.wa_version,"setHash":lock.set_hash,"dropsByReason":diag.drops_by_reason,"wamGapSites":sites}) + &json!({"qualityReportVersion":1,"waVersion":lock.wa_version,"setHash":lock.set_hash,"dropsByReason":diag.drops_by_reason,"unlocatedDropsByReason":unlocated,"wamGapSites":sites}) )? ); Ok(()) @@ -87,3 +88,65 @@ mod tests { assert_eq!(counted, normal); } } + +fn unlocated_counts( + counts: &BTreeMap, + sites: &[serde_json::Value], +) -> Result> { + let mut unlocated = counts.clone(); + for site in sites { + let reason = site["reason"].as_str().context("gap site reason missing")?; + let count = unlocated + .get_mut(reason) + .with_context(|| format!("uncounted gap site: {reason}"))?; + ensure!(*count > 0, "more located sites than counted gaps: {reason}"); + *count -= 1; + } + unlocated.retain(|_, count| *count != 0); + Ok(unlocated) +} + +#[cfg(test)] +mod unlocated_tests { + #[test] + fn extracted_global_gap_is_not_presented_as_located() { + let source = r#"__d("Globals",["WAWebWamCodegenUtils"],function(t,n,r,o,a,i,l){var e=o("WAWebWamCodegenUtils");l.Global=e.defineGlobal({computed:[3,e.TYPES.STRING,[CHANNEL]],empty:[4,e.TYPES.STRING,[]]});});"#; + let defs = super::counted_definitions(source).unwrap(); + let (_, diag, sites) = wa_wam::extract_wam_with_gap_sites(source, &defs, "test"); + assert!(sites.is_empty()); + assert_eq!( + diag.drops_by_reason["global with an unreadable channel list"], + 2 + ); + assert_eq!( + super::unlocated_counts(&diag.drops_by_reason, &[]).unwrap(), + diag.drops_by_reason + ); + } + + #[test] + fn overlocated_counts_are_rejected_instead_of_clamped() { + let counts = std::collections::BTreeMap::from([("reason".to_string(), 1)]); + let sites = vec![serde_json::json!({"reason":"reason"}); 2]; + assert!(super::unlocated_counts(&counts, &sites).is_err()); + } + + #[test] + fn global_gaps_are_explicitly_unlocated() { + let counts = std::collections::BTreeMap::from([ + ("global with an unreadable channel list".to_string(), 1), + ("written key naming no field of the event".to_string(), 2), + ]); + let sites = vec![ + serde_json::json!({"reason":"written key naming no field of the event"}), + serde_json::json!({"reason":"written key naming no field of the event"}), + ]; + assert_eq!( + super::unlocated_counts(&counts, &sites).unwrap(), + std::collections::BTreeMap::from([( + "global with an unreadable channel list".to_string(), + 1 + ),]) + ); + } +} From 5320071733c1ac92547ae79db298adee3c45837b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:19:26 +0000 Subject: [PATCH 30/52] fix: mark unbound snapshot input provenance as unknown --- crates/whatspec/src/contract_diff.rs | 77 ++++++++++++++++--- docs/snapshot-quality-2026-10-07.md | 20 ++++- docs/snapshot-quality-2026-10-07.reviews.json | 6 +- docs/snapshot-quality.md | 26 +++++-- 4 files changed, 109 insertions(+), 20 deletions(-) diff --git a/crates/whatspec/src/contract_diff.rs b/crates/whatspec/src/contract_diff.rs index 2680bab6..c5af2480 100644 --- a/crates/whatspec/src/contract_diff.rs +++ b/crates/whatspec/src/contract_diff.rs @@ -6,7 +6,7 @@ use std::path::{Component, Path}; use anyhow::{Context, Result, bail, ensure}; use serde_json::{Value, json}; -const FORMAT_VERSION: u32 = 1; +const FORMAT_VERSION: u32 = 2; fn pointer(key: &str) -> String { key.replace('~', "~0").replace('/', "~1") @@ -94,8 +94,10 @@ impl Snapshot { fn metadata(&self) -> Value { json!({"waVersion": self.manifest["waVersion"], "schemaVersion": self.manifest["schemaVersion"], - "generatorVersion": self.manifest["generatorVersion"], "setHash": self.lock.set_hash, - "bundleCount": self.lock.bundle_count}) + "generatorVersion": self.manifest["generatorVersion"], + "declaredInputs": {"setHash": self.lock.set_hash, "bundleCount": self.lock.bundle_count, + "lockStatus":"self-consistent"}, + "inputBinding": {"status":"unverified", "reason":"manifest-does-not-bind-inputs"}}) } } @@ -308,10 +310,11 @@ pub fn report(old: &Path, new: &Path, evidence: Option<&Path>) -> Result // that document even though there is no field pointer to reference. change["oldArtifactSha256"] = json!(a.map(|(_, hash, _)| hash)); change["newArtifactSha256"] = json!(b.map(|(_, hash, _)| hash)); - // Bind a review to exact artifact contents AND inputs, not just a name - // that can survive an unrelated future snapshot. - change["oldSetHash"] = json!(old.lock.set_hash); - change["newSetHash"] = json!(new.lock.set_hash); + // Bind reviews to artifact contents and declared locks. A valid lock + // does not prove which bundle set produced an unbound manifest. + change["oldDeclaredSetHash"] = json!(old.lock.set_hash); + change["newDeclaredSetHash"] = json!(new.lock.set_hash); + change["inputBinding"] = json!("unverified"); change["id"] = json!(wa_text::sha256_hex( serde_json::to_string(&change)?.as_bytes() )); @@ -336,9 +339,11 @@ pub fn report(old: &Path, new: &Path, evidence: Option<&Path>) -> Result } Ok( json!({"reportVersion": FORMAT_VERSION, "old": old.metadata(), "new": new.metadata(), - "sameInputs": old.lock.set_hash == new.lock.set_hash, "classifications": classifications, + "sameInputs": null, "sameDeclaredInputs": old.lock.set_hash == new.lock.set_hash, + "classifications": classifications, "changes": changes, "oldDiagnostics": old.manifest["diagnostics"], "newDiagnostics": new.manifest["diagnostics"], - "limits": ["No automatic rename matching or compatibility verdict.", "Reviews are supplied evidence assessments, not machine proofs.", + "limits": ["Historical manifests do not bind their inputs. Lock consistency and matching versions cannot establish actual input identity; sameInputs is unknown.", + "No automatic rename matching or compatibility verdict.", "Reviews are supplied evidence assessments, not machine proofs.", "Source module hints may require following mixins or other dependencies.", "Unkeyed and ambiguous arrays retain order; protobuf is an opaque artifact delta."]}), ) } @@ -522,10 +527,60 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } + #[test] + fn same_version_lock_swap_does_not_claim_verified_inputs() { + let dir = + std::env::temp_dir().join(format!("whatspec-unbound-inputs-{}", std::process::id())); + let write = |name: &str, bundle: &[u8]| { + let root = dir.join(name); + std::fs::create_dir_all(&root).unwrap(); + let lock = wa_store::lock::BundleLock::new( + "test", + vec![wa_store::lock::BundleId { + sha256: wa_text::sha256_hex(bundle), + size: bundle.len() as u64, + url: None, + }], + ); + std::fs::write(root.join("bundles.lock.json"), lock.to_pretty_json()).unwrap(); + let doc = json!({"waVersion":"test", "schemaVersion":"4.3.0"}).to_string(); + std::fs::write(root.join("index.json"), &doc).unwrap(); + let manifest = json!({"waVersion":"test", "schemaVersion":"4.3.0", "domains":{"test":{"file":"index.json", "sha256":wa_text::sha256_hex(doc.as_bytes())}}}); + std::fs::write(root.join("manifest.json"), manifest.to_string()).unwrap(); + root + }; + let old = write("old", b"bundle-a"); + let new = write("new", b"bundle-b"); // Identical version, manifest and artifacts; a different valid lock. + let result = report(&old, &new, None).unwrap(); + assert!(result["sameInputs"].is_null()); + assert_eq!(result["sameDeclaredInputs"], false); + assert_eq!(result["old"]["inputBinding"]["status"], "unverified"); + assert_eq!(result["new"]["inputBinding"]["status"], "unverified"); + assert_ne!( + result["old"]["declaredInputs"]["setHash"], + result["new"]["declaredInputs"]["setHash"] + ); + let same = report(&old, &old, None).unwrap(); + assert!(same["sameInputs"].is_null()); + assert_eq!(same["sameDeclaredInputs"], true); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn v1_reviews_are_rejected_after_input_provenance_semantics_change() { + let old = json!({"reportVersion":1, "reviews":[]}); + assert!( + apply_reviews(&mut [], &old) + .unwrap_err() + .to_string() + .contains("unsupported") + ); + } + #[test] fn reviewed_causes_are_explicit_and_cannot_be_renames() { let mut changes = [json!({"id":"current"})]; - let mut review = json!({"reportVersion":1,"reviews":[{"id":"current","classification":"extraction-loss","basis":"field remains in source","references":["bundle SHA-256 and byte span"]}]}); + let mut review = json!({"reportVersion":2,"reviews":[{"id":"current","classification":"extraction-loss","basis":"field remains in source","references":["bundle SHA-256 and byte span"]}]}); apply_reviews(&mut changes, &review).unwrap(); assert_eq!(changes[0]["assessment"]["origin"], "reviewed-evidence"); review["reviews"][0]["classification"] = json!("rename"); @@ -588,7 +643,7 @@ mod tests { #[test] fn stale_reviews_fail_instead_of_reclassifying_another_snapshot() { - let review = json!({"reportVersion":1,"reviews":[{"id":"wrong","classification":"upstream-change","basis":"verified","references":["bundle:offset"]}]}); + let review = json!({"reportVersion":2,"reviews":[{"id":"wrong","classification":"upstream-change","basis":"verified","references":["bundle:offset"]}]}); assert!( apply_reviews(&mut [json!({"id":"current"})], &review) .unwrap_err() diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index 8e09f0f9..def0830d 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -200,9 +200,27 @@ occurrence, with the field name, rather than a counter without source evidence. Filtered source sidecars now explicitly record their requested module names; absence findings are limited to that selection. -The generic contract report contains 486 deltas (2,652,610 bytes), keeps ordered +The generic contract report contains 486 deltas (2,678,428 bytes), keeps ordered arrays and does not infer renames. The [review ledger](snapshot-quality-2026-10-07.reviews.json) classifies the privacy improvement and persisted-ID change; the remaining 484 deltas stay indeterminate until reviewed. `--evidence` binds each review to both artifact hashes. The baseline dispositions above concern those six counters, not all contract differences. + + +## Manifest/lock binding limitation + +A same-version lock-swap regression confirms that the historical manifest does +not bind its document hashes to the lock's bundle set. A valid replacement lock +was accepted and previously gave an authoritative-looking `sameInputs` verdict. +Report version 2 retains historical comparison but marks the input binding +unverified, reports declared lock identities separately, and returns unknown +(`null`) for `sameInputs`. No hashes are fabricated or backfilled into manifests; +the existing manual restore/regeneration evidence remains distinct from what the +comparison command can verify on its own. Version-1 review ledgers must migrate. + +The exact global-channel fixture recorded in #55 (set hash +`840dfc3f996afcfac451f80043580175b4443db0487ec4c056ffb02bc9fd57be`) +reproduces two drops with zero construction sites. These remain unlocated and +are now explicitly reported as one `unlocatedDropsByReason` counter of two; +no complete source-attribution claim is made for them. diff --git a/docs/snapshot-quality-2026-10-07.reviews.json b/docs/snapshot-quality-2026-10-07.reviews.json index afd77453..81f91b5d 100644 --- a/docs/snapshot-quality-2026-10-07.reviews.json +++ b/docs/snapshot-quality-2026-10-07.reviews.json @@ -1,8 +1,8 @@ { - "reportVersion": 1, + "reportVersion": 2, "reviews": [ { - "id": "44f5730a6c4282f12bd9200b9d8864a3e3beb75f7b3daa6fb0dc074a1d269aa0", + "id": "8fe1a63820214e0217e2ce355b3728c03b129312df3cc3f11274f703c88dec52", "classification": "extraction-improvement", "basis": "The upstream helper changes from four positional arguments to one object. Its category attributes and repeated user child remain present. The new IR recovers name/value/dhash and users[] paths; the old user paths incorrectly passed through name. This does not establish full branch coverage.", "references": [ @@ -11,7 +11,7 @@ ] }, { - "id": "4c8e12058941be16491dc7de2abd60931c52c9405fa962eb8f262bc38972b3b0", + "id": "0c77cf9ebec284161c2884867101bcfb5fb9faacb8e4f5bc7adc0439f39caaa4", "classification": "upstream-change", "basis": "The preserved facebookRelayOperation module exports literal 27747880408206174 in the old set and 27796221486653417 in the new set. Both values agree with their IR. This establishes a persisted operation ID change in these snapshots, not server compatibility.", "references": [ diff --git a/docs/snapshot-quality.md b/docs/snapshot-quality.md index 6bf4ad9c..ad0d85bf 100644 --- a/docs/snapshot-quality.md +++ b/docs/snapshot-quality.md @@ -24,12 +24,26 @@ python3 -m unittest discover -s scripts/tests -v cargo run --release -p whatspec -- diff old-generated generated --json > contracts.json ``` -The JSON report has its own `reportVersion: 1`. It is an additive CLI output, -not a change to IR `schemaVersion`. No runtime consumer policy changes. In +The JSON report has its own `reportVersion: 2`. Version 2 separates declared +locks from verified input provenance. Historical manifests do not bind their +artifacts to a bundle-set hash: matching `waVersion`, valid artifact hashes, and +a self-consistent lock cannot prove which bundle set produced the artifacts. +Accordingly `inputBinding.status` is `unverified` and `sameInputs` is `null`, even +when the two declared locks match. `declaredInputs` and `sameDeclaredInputs` +report those lock declarations explicitly; delta IDs bind `oldDeclaredSetHash` +and `newDeclaredSetHash` without presenting them as verified build inputs. + +Historical snapshots remain comparable without fabricated bindings. No manifest +field is backfilled, and no IR or generator schema changes. A future verified +binding needs a coordinated producer/manifest contract. Version-1 review ledgers +are rejected; regenerate IDs and use `reportVersion: 2` when migrating. + +This changes only the optional report contract, +not IR `schemaVersion`. No runtime consumer policy changes. In particular, strict maintenance checks do not require clients to reject unknown extensions in received traffic. -Each delta includes before/after values, document SHA-256, JSON Pointers, bundle +Each delta includes before/after values, document SHA-256, JSON Pointers, declared bundle set hashes, and a stable ID bound to those values. Enums use `(module, name)`. MEX and app-state use the document's map keys. Other recognized top-level collections use explicit keys listed in `contract_diff.rs`. Duplicate keys stay @@ -83,7 +97,9 @@ bundle locations. It checks the same complete bundle set and parse coverage. Two reasons may point at one construction; repeated copies of a module follow the extractor's existing deduplication. Counted diagnostics use the same outer module selection as normal extraction, avoiding overlapping scans of nested -definitions. The provenance index still retains nested definitions. The optional sidecar does not enlarge the +definitions. The provenance index still retains nested definitions. Counters without construction locations (such as unreadable global channel +lists) are explicitly separated in `unlocatedDropsByReason`; an empty site list +is not a claim of full source attribution. The optional sidecar does not enlarge the IR or change the existing `WamDiagnostics` API. Compare reports from the same extractor revision for upstream changes, and from two revisions on one locked set for extraction changes. Neither module names nor minified offsets establish @@ -108,7 +124,7 @@ Store reviewed assessments in a small JSON file: ```json { - "reportVersion": 1, + "reportVersion": 2, "reviews": [{ "id": "exact ID from contracts.json", "classification": "upstream-change", From 2b97bd812f242e8750f7122533ffa16e8b798df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:26:57 +0000 Subject: [PATCH 31/52] fix(wam): honor constructor merge overwrite order --- crates/wa-wam/src/callsites.rs | 37 +- crates/wa-wam/src/tests.rs | 30 ++ docs/snapshot-quality-2026-10-07.md | 34 +- docs/snapshot-quality-2026-10-07.sources.json | 422 ++++++++++++++++++ generated/manifest.json | 12 +- generated/wam/index.json | 341 +------------- 6 files changed, 540 insertions(+), 336 deletions(-) diff --git a/crates/wa-wam/src/callsites.rs b/crates/wa-wam/src/callsites.rs index f340f414..1bcf19a6 100644 --- a/crates/wa-wam/src/callsites.rs +++ b/crates/wa-wam/src/callsites.rs @@ -407,9 +407,42 @@ fn read_argument( if as_object(e).is_some() || locals.is_some_and(|values| values.contains_key(&e.span().start)) => { - read_argument(e, fields, partial, unread, aliases, locals) + let mut operand = Vec::new(); + let mut operand_partial = false; + read_argument( + e, + &mut operand, + &mut operand_partial, + unread, + aliases, + locals, + ); + if operand_partial { + // An unresolved key or spread can overwrite any earlier value, + // including a value within this operand. + for (_, _, value) in fields.iter_mut().chain(operand.iter_mut()) { + *value = None; + } + *partial = true; + } + for (name, write, value) in operand { + // Object.assign semantics: later operands overwrite earlier + // ones. Keep occurrences for diagnostics, but agree on the + // final constructor value before merging later event writes. + for (previous_name, _, previous_value) in fields.iter_mut() { + if previous_name == &name { + *previous_value = value.clone(); + } + } + fields.push((name, write, value)); + } + } + _ => { + for (_, _, value) in fields.iter_mut() { + *value = None; + } + *partial = true; } - _ => *partial = true, } } return; diff --git a/crates/wa-wam/src/tests.rs b/crates/wa-wam/src/tests.rs index de2ff37d..a1d8a0ad 100644 --- a/crates/wa-wam/src/tests.rs +++ b/crates/wa-wam/src/tests.rs @@ -924,3 +924,33 @@ fn unknown_field_gaps_have_one_location_per_counted_write() { std::collections::BTreeSet::from(["notInCatalog", "alsoUnknown"]) ); } + +#[test] +fn constructor_merges_follow_operand_order_without_overstating_unknown_overrides() { + for (operands, expected, partial) in [ + ("{},fields,{retryCount:4}", Some(4), false), + ("{},{retryCount:4},fields", Some(3), false), + ("{},fields,unknown(),{retryCount:4}", Some(4), true), + ("{},fields,unknown()", None, true), + ("{},fields,{...unknown()}", None, true), + ("{},fields,{[unknown()]:4}", None, true), + ("{},fields,{...unknown()},{retryCount:4}", Some(4), true), + ] { + let source = format!( + "{LOCAL_ARGUMENT_CATALOG}__d(\"Reporter\",[\"WAWebMessageSendWamEvent\"],function(){{function send(){{var fields={{retryCount:3}};new(o(\"WAWebMessageSendWamEvent\")).MessageSendWamEvent(babelHelpers.extends({operands}));}}}});" + ); + let (ir, _) = run_full(&source); + let site = &ir + .events + .iter() + .find(|e| e.name == "MessageSend") + .unwrap() + .call_sites[0]; + assert_eq!(site.partial, partial, "{operands}"); + assert_eq!( + site.fields[0].value, + expected.map(|value| WamCallSiteValue::Int { value }), + "{operands}" + ); + } +} diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index def0830d..a39ddcfa 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -154,6 +154,32 @@ unchanged. Both source modules are in the focused sidecar. Regression tests reproduce the omitted merge fields and preserve partial status for unknown operands, escaped locals, mutated targets and outer closure reads. + +A second merge regression reproduces a lost final value: merging the approved +local `{retryCount:3}` before `{retryCount:4}` must retain `4`. Constructor +operands now follow overwrite order; later event writes retain their existing +conservative treatment. An unread later operand may overwrite earlier values, +so those values are withdrawn until its keys can be established. Seven fixture +cases cover both operand orders, unknown calls, computed keys and spreads. + +On the exact current bundle set, this correction changes ten module/event +pairs. Ignoring values, their field/write shapes are identical before and after; +all 874 constructions remain counted. Six sites become duplicates after value +withdrawal: WebcMemoryStat loses one duplicate, EditBusinessProfile two, +CtwaOrderSignal one and PaidMessagingUserInteractionsLogger two. The other six +changed groups are StatusReply, ChatMute, AttachmentTrayActions, +PsGroupExitExperienceExitDialogInteraction, SmbQpCallHealth and +CompanionInviteContact. All ten modules have exact source locators in the +focused sidecar. Their unsupported later operands include calls, identifiers, +conditional objects and logical expressions. This is a conservative correction +to extraction certainty, not an upstream removal. Resolving those operand key +sets could recover values later; the current extractor does not prove them. + +The final totals are 807 sites, 3,453 field occurrences, 769 values and 107 +partial sites. The write guard caught 813 → 807; an isolated generation and the +field/write-shape comparison established the six deduplications before replacing +the generated WAM/manifest. No guard, floor or diagnostic baseline was lowered. + The nested-definition provenance review separately exposed overlapping WAM scans. Its regression counted each gap twice before the fix. `quality-gaps` now uses the normal extractor's outer module selection, while the source index @@ -182,9 +208,9 @@ cargo test -p wa-transform -p wa-wam -p whatspec --all-features ``` Source indexes are generated on demand (~14.2 MB each), not committed wholesale. -The focused source and gap ledgers are ~86.5 KB and ~21.4 KB. Two real WAM fixtures +The focused source and gap ledgers are ~104.6 KB and ~21.4 KB. Two real WAM fixtures retain exact source slices, with bundle hashes and offsets in their headers. -The WAM document grows from 2,794,121 to 2,816,845 bytes by recovering existing +The WAM document grows from 2,794,121 to 2,807,208 bytes by recovering existing field semantics; optional provenance adds no per-field IR metadata. WAM gap evidence incurs extra parsing only when requested. At the initial local-object revision, regeneration checks reproduce all 27 artifacts for the repaired current snapshot and a separately regenerated old snapshot. Observed check times were @@ -200,10 +226,10 @@ occurrence, with the field name, rather than a counter without source evidence. Filtered source sidecars now explicitly record their requested module names; absence findings are limited to that selection. -The generic contract report contains 486 deltas (2,678,428 bytes), keeps ordered +The generic contract report contains 495 deltas (2,725,481 bytes), keeps ordered arrays and does not infer renames. The [review ledger](snapshot-quality-2026-10-07.reviews.json) classifies the privacy -improvement and persisted-ID change; the remaining 484 deltas stay indeterminate until +improvement and persisted-ID change; the remaining 493 deltas stay indeterminate until reviewed. `--evidence` binds each review to both artifact hashes. The baseline dispositions above concern those six counters, not all contract differences. diff --git a/docs/snapshot-quality-2026-10-07.sources.json b/docs/snapshot-quality-2026-10-07.sources.json index 9c6f05ea..1c410304 100644 --- a/docs/snapshot-quality-2026-10-07.sources.json +++ b/docs/snapshot-quality-2026-10-07.sources.json @@ -62,6 +62,55 @@ "start": 141037 } ], + "WAWebAttachmentMenuLogger": [ + { + "bundleSha256": "034314e49fa571a8e2e8491843a15536326b4a47d1f66ac7704d2332ee1b391e", + "dependencies": [ + "$InternalEnum", + "WAWebAttachmentTrayActionsWamEvent", + "WAWebChatGetters", + "WAWebWamEnumAttachmentTrayActionTargetType", + "WAWebWamEnumAttachmentTrayActionType", + "WAWebWamNumberToClientGroupSizeBucket", + "nullthrows" + ], + "end": 3232816, + "factorySha256": "095a938a6c71cbd6d01ab226c282561fa2c16e528a5b1bff4c30aabe06554910", + "moduleSha256": "39ab20991281aad8818bbaca7bee0979531d9aedaf12974005684837945488f6", + "start": 3230163 + }, + { + "bundleSha256": "9d3897273785385ceffb16bd782418bf1460264ee1ad124fcd2b86a96ce1b03d", + "dependencies": [ + "$InternalEnum", + "WAWebAttachmentTrayActionsWamEvent", + "WAWebChatGetters", + "WAWebWamEnumAttachmentTrayActionTargetType", + "WAWebWamEnumAttachmentTrayActionType", + "WAWebWamNumberToClientGroupSizeBucket", + "nullthrows" + ], + "end": 18888, + "factorySha256": "095a938a6c71cbd6d01ab226c282561fa2c16e528a5b1bff4c30aabe06554910", + "moduleSha256": "39ab20991281aad8818bbaca7bee0979531d9aedaf12974005684837945488f6", + "start": 16235 + } + ], + "WAWebChatMuteLogger": [ + { + "bundleSha256": "6c83d155d35d8a7086b9ece97922d02acb37a603f5df1dc2c2107f347ae9283f", + "dependencies": [ + "WAWebChatGetters", + "WAWebChatMuteWamEvent", + "WAWebWamEnumActionConducted", + "WAWebWamEnumMuteChatType" + ], + "end": 45047, + "factorySha256": "6e9509bb3384401c9157f4280708aeeea61c000a42f176ee2428ac81b6b92109", + "moduleSha256": "1bf6c004215c3d05237eff06af5983e3cd2b27ba51865d2ec4877c9bb2a2152e", + "start": 43964 + } + ], "WAWebContactManagerCustomerProfilesQuery": [ { "bundleSha256": "792466f7a00cd5862ac782f916733975cd9f46800bb9fd9679a1301411c24508", @@ -166,6 +215,20 @@ "start": 369272 } ], + "WAWebEditFormLogEvents": [ + { + "bundleSha256": "e1f2135976735b8b3c1c3807780c1ce0a276f3e2cb1e4fd0fd342fd541ba45f1", + "dependencies": [ + "WAWebEditBusinessProfileWamEvent", + "WAWebWamEnumBusinessProfileEntryPoint", + "WAWebWamEnumEditProfileAction" + ], + "end": 34172, + "factorySha256": "97f1b68ad5fefc6d61f8e3f1d0fa3c18f5bdc34b3c97cd71237008f70029257f", + "moduleSha256": "9a65ed06c77b42abcf4f83c101bc70ed453dea8e74fa34d5ca76c8c8aa848f64", + "start": 31405 + } + ], "WAWebFilterLogging": [ { "bundleSha256": "15339e541ca850185a14c0603d262eed300ad5019b65d3582aaf3a8eeab1cc96", @@ -192,6 +255,45 @@ "start": 500906 } ], + "WAWebGroupExitModalLoggingUtils": [ + { + "bundleSha256": "fd26924373be0ce0e773c304511df62909fa76e11d9294c0509542d3f38f842d", + "dependencies": [ + "WAWebPsGroupExitExperienceExitDeleteConfirmationDialogUiInteractionWamEvent", + "WAWebPsGroupExitExperienceExitDialogInteractionWamEvent", + "WAWebPsGroupExitExperienceGroupActionWamEvent", + "WAWebSpamConstants", + "WAWebWamEnumGroupExitExperienceOrigin", + "WAWebWamEnumPsGroupExitExperienceActions", + "WAWebWamEnumPsGroupExitExperienceDeleteConfirmationDialogActions", + "WAWebWamEnumPsGroupExitExperienceExitDialogActions" + ], + "end": 387978, + "factorySha256": "fe9058df08fabf78e5110335b0b938b713abc6a647c7f7699fb867530a16b0a6", + "moduleSha256": "7a7c86020d8023d7ac43439513a7d303fd0f0acfbe037114c6c255ff9c57166c", + "start": 383109 + } + ], + "WAWebInteractiveMessageCarouselLogEvents": [ + { + "bundleSha256": "cf6197f616a43cf9004101d911d5643e8fa63972b2d27a54e8bba8db062abb8f", + "dependencies": [ + "WALogger", + "WAWebInteractiveMessageHeaderMediaType", + "WAWebMsgCollection", + "WAWebPaidMessagingUserInteractionsLoggerWamEvent", + "WAWebWamEnumPaidMessagingUserInteractionsActionTarget", + "WAWebWamEnumPaidMessagingUserInteractionsActionType", + "WAWebWamEnumPaidMessagingUserInteractionsComponentType", + "WAWebWamEnumPaidMessagingUserInteractionsHeaderMediaType", + "WAWebWamEnumPaidMessagingUserInteractionsMarketingFormat" + ], + "end": 718950, + "factorySha256": "8b18a15fe4f431036d89b0e5444aae58df52cc7783e56e717c986b77816ff332", + "moduleSha256": "1e7a9a8e67c3ea5e9f173c51ee16458f3181862346d90cef938458114bdcc2d7", + "start": 713460 + } + ], "WAWebKeepInChatMsgAction": [ { "bundleSha256": "034314e49fa571a8e2e8491843a15536326b4a47d1f66ac7704d2332ee1b391e", @@ -359,6 +461,28 @@ "start": 3406431 } ], + "WAWebLogStatusReply": [ + { + "bundleSha256": "72622fd528bcdb8719660018f754e37ce201276a4c5fdead04f82bc81648de8d", + "dependencies": [ + "WAWebConnModel", + "WAWebFrontendContactGetters", + "WAWebGroupMetadataCollection", + "WAWebMsgGetters", + "WAWebStatusLoggingUtils", + "WAWebStatusReplyWamEvent", + "WAWebUnifiedSession", + "WAWebWamEnumReplyEntryMethod", + "WAWebWamEnumStatusCategory", + "WAWebWamEnumStatusPosterContactType", + "asyncToGeneratorRuntime" + ], + "end": 310869, + "factorySha256": "7ec0de7d9423c74f32ee83ca207898e2694ffcc22808009d3ed95f4ceac89e4d", + "moduleSha256": "c599680d4b3c5906322fd830518243770768cc55f160561a46e3beaaa0003b7b", + "start": 308936 + } + ], "WAWebMexLogging": [ { "bundleSha256": "657a32007737e50b0cb0292ce9c364e21e48291b558129753b25e3ec2a4c13b7", @@ -371,6 +495,26 @@ "start": 2398464 } ], + "WAWebOutContactLoggingUtils": [ + { + "bundleSha256": "ed2f08bc12a1128a06ddafe25fc62e5d102593245a8b5e818fd8f2eedab3ffb9", + "dependencies": [ + "WAWebCompanionInviteContactWamEvent", + "WAWebContactCollection", + "WAWebOutContactCollection", + "WAWebOutContactInviteJourney", + "WAWebOutContactInviteUtils", + "WAWebWamEnumCompanionInviteActionType", + "WAWebWamEnumCompanionInviteMethodType", + "WAWebWamEnumCompanionInviteOriginType", + "asyncToGeneratorRuntime" + ], + "end": 111346, + "factorySha256": "11d3c6bb1bc65e80af4542d2e00f8b1b27bf0cb87f89aea6bd2ab5702310ab39", + "moduleSha256": "9432c365f05a84032454be3b7c10d57ff5618cd8175f1343b7bf1c5c883eea47", + "start": 108105 + } + ], "WAWebPaymentsUserActionLogging": [ { "bundleSha256": "c54c33dd6f74be053881c3a7d0e750f8b8fa04140579821b214ab6f59711bbe0", @@ -548,6 +692,20 @@ "start": 681256 } ], + "WAWebSmbQpCallHealthLogger": [ + { + "bundleSha256": "d8530b0e3d68a35306e30118b34f7a3e142816b3365e76f809cbcd765307df34", + "dependencies": [ + "WAWebSmbQpCallHealthWamEvent", + "WAWebWamEnumFetchMechanismEnum", + "WAWebWamEnumFetchResultEnum" + ], + "end": 1338673, + "factorySha256": "44889e0ea5a53becf02caadda4b55f7e08c1b4d8eb0db920ccdc32180d5066b0", + "moduleSha256": "052f116f9ae291854880570c31095ba8962de655517aad5de3efc0efc39bd4e7", + "start": 1338052 + } + ], "WAWebStatusSubtitle.react": [ { "bundleSha256": "72622fd528bcdb8719660018f754e37ce201276a4c5fdead04f82bc81648de8d", @@ -903,6 +1061,32 @@ "start": 3445837 } ], + "WAWebWamMemoryStat": [ + { + "bundleSha256": "82f783508805f734f16747c4bd0670a496f13004e8a0acd11fe5f45efaffd754", + "dependencies": [ + "Promise", + "WALogger", + "WAShiftTimer", + "WAWebAppTracker", + "WAWebBackendApi", + "WAWebEnvironment", + "WAWebLidAwareContactsDB", + "WAWebMemoryStatWamEvent", + "WAWebRuntimeEnvironmentUtils", + "WAWebSchemaChat", + "WAWebSchemaMessage", + "WAWebWamEnumWebcScenarioType", + "WAWebWebcMemoryStatWamEvent", + "asyncToGeneratorRuntime", + "nullthrows" + ], + "end": 195115, + "factorySha256": "1b02243900bdc22f3061d236781be24099072edb67f2c8d27ec6fefa2bf335d9", + "moduleSha256": "b01e97129f29b855a5c5878c15a98cc4477ed8cc4a0309ab097173b7450fd5b8", + "start": 190059 + } + ], "WAWebWamTypeHash": [ { "bundleSha256": "657a32007737e50b0cb0292ce9c364e21e48291b558129753b25e3ec2a4c13b7", @@ -1246,6 +1430,8 @@ "EBMessageMetadataQueryQuery.graphql", "EBMessageMetadataQueryQuery_facebookRelayOperation", "WAWebAccountLinkingAPIGetCertsQuery.graphql", + "WAWebAttachmentMenuLogger", + "WAWebChatMuteLogger", "WAWebContactManagerCustomerProfilesQuery", "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation", "WAWebCreateLabyrinthBackupJob", @@ -1254,12 +1440,17 @@ "WAWebDebugLabyrinthInboxSnapshotQuery.graphql", "WAWebDebugLabyrinthRangeQuery", "WAWebDebugLabyrinthRangeQuery.graphql", + "WAWebEditFormLogEvents", "WAWebFilterLogging", + "WAWebGroupExitModalLoggingUtils", + "WAWebInteractiveMessageCarouselLogEvents", "WAWebKeepInChatMsgAction", "WAWebKmpSyncdRequestBuilder", + "WAWebLogStatusReply", "WAWebMexLogging", "WAWebMexUpdateNewsletterAdminProfileSettingJob", "WAWebMexUpdateNewsletterAdminProfileSettingJobMutation.graphql", + "WAWebOutContactLoggingUtils", "WAWebPaymentsUserActionLogging", "WAWebPixWamLogger", "WAWebRotateLabyrinthEpochJob", @@ -1269,6 +1460,7 @@ "WAWebSetPrivacyJob", "WAWebSmb3pdAggregatedConversionSignalAction", "WAWebSmbMarkAsXOrderAction", + "WAWebSmbQpCallHealthLogger", "WAWebStatusSubtitle.react", "WAWebStatusViewerActionWamEvent", "WAWebTeamLinkCreateInvitationDialog.react", @@ -1292,6 +1484,7 @@ "WAWebUploadLabyrinthMessagesJobMutation.graphql", "WAWebWamCodegenUtils", "WAWebWamFingerprintReporter", + "WAWebWamMemoryStat", "WAWebWamTypeHash", "WAWebWindowsHybridBridgeWam.v2589", "WAWebWindowsHybridBridgeWam.v2590", @@ -1341,6 +1534,55 @@ "start": 2181768 } ], + "WAWebAttachmentMenuLogger": [ + { + "bundleSha256": "7d75ce3fb8d9c7f0645869634d4379c6516b437918f4124649615004ae0fade1", + "dependencies": [ + "$InternalEnum", + "WAWebAttachmentTrayActionsWamEvent", + "WAWebChatGetters", + "WAWebWamEnumAttachmentTrayActionTargetType", + "WAWebWamEnumAttachmentTrayActionType", + "WAWebWamNumberToClientGroupSizeBucket", + "nullthrows" + ], + "end": 18888, + "factorySha256": "095a938a6c71cbd6d01ab226c282561fa2c16e528a5b1bff4c30aabe06554910", + "moduleSha256": "39ab20991281aad8818bbaca7bee0979531d9aedaf12974005684837945488f6", + "start": 16235 + }, + { + "bundleSha256": "9b681e073f14f9d50e3dc12b456e4f980b554eff4d58827bc91c90a697e9ca38", + "dependencies": [ + "$InternalEnum", + "WAWebAttachmentTrayActionsWamEvent", + "WAWebChatGetters", + "WAWebWamEnumAttachmentTrayActionTargetType", + "WAWebWamEnumAttachmentTrayActionType", + "WAWebWamNumberToClientGroupSizeBucket", + "nullthrows" + ], + "end": 2996308, + "factorySha256": "095a938a6c71cbd6d01ab226c282561fa2c16e528a5b1bff4c30aabe06554910", + "moduleSha256": "39ab20991281aad8818bbaca7bee0979531d9aedaf12974005684837945488f6", + "start": 2993655 + } + ], + "WAWebChatMuteLogger": [ + { + "bundleSha256": "df74e44978c7d32ee20a0fe2cb1abce4a0d74a3bedfa6288a6314e274f2d10eb", + "dependencies": [ + "WAWebChatGetters", + "WAWebChatMuteWamEvent", + "WAWebWamEnumActionConducted", + "WAWebWamEnumMuteChatType" + ], + "end": 42016, + "factorySha256": "6e9509bb3384401c9157f4280708aeeea61c000a42f176ee2428ac81b6b92109", + "moduleSha256": "1bf6c004215c3d05237eff06af5983e3cd2b27ba51865d2ec4877c9bb2a2152e", + "start": 40933 + } + ], "WAWebContactManagerCustomerProfilesQuery": [ { "bundleSha256": "36dc1290c51b02500ad640ba5aa793dd1b5e9172afcb71ff327d64cd1f279717", @@ -1374,6 +1616,20 @@ "start": 252637 } ], + "WAWebEditFormLogEvents": [ + { + "bundleSha256": "7783a694b3a8bc3a379585817de0e477c75729c73b21adbffbf060f5f0ac2860", + "dependencies": [ + "WAWebEditBusinessProfileWamEvent", + "WAWebWamEnumBusinessProfileEntryPoint", + "WAWebWamEnumEditProfileAction" + ], + "end": 86839, + "factorySha256": "97f1b68ad5fefc6d61f8e3f1d0fa3c18f5bdc34b3c97cd71237008f70029257f", + "moduleSha256": "9a65ed06c77b42abcf4f83c101bc70ed453dea8e74fa34d5ca76c8c8aa848f64", + "start": 84072 + } + ], "WAWebFilterLogging": [ { "bundleSha256": "3759ae9b91f03d11301433abb539dbe1cf575b9ba83d71cac768aa5fd662f76a", @@ -1401,6 +1657,80 @@ "start": 399753 } ], + "WAWebGroupExitModalLoggingUtils": [ + { + "bundleSha256": "9b681e073f14f9d50e3dc12b456e4f980b554eff4d58827bc91c90a697e9ca38", + "dependencies": [ + "WAWebPsGroupExitExperienceExitDeleteConfirmationDialogUiInteractionWamEvent", + "WAWebPsGroupExitExperienceExitDialogInteractionWamEvent", + "WAWebPsGroupExitExperienceGroupActionWamEvent", + "WAWebSpamConstants", + "WAWebWamEnumGroupExitExperienceOrigin", + "WAWebWamEnumPsGroupExitExperienceActions", + "WAWebWamEnumPsGroupExitExperienceDeleteConfirmationDialogActions", + "WAWebWamEnumPsGroupExitExperienceExitDialogActions" + ], + "end": 5210584, + "factorySha256": "fe9058df08fabf78e5110335b0b938b713abc6a647c7f7699fb867530a16b0a6", + "moduleSha256": "7a7c86020d8023d7ac43439513a7d303fd0f0acfbe037114c6c255ff9c57166c", + "start": 5205715 + }, + { + "bundleSha256": "ee46bf4858a1118a5a2c15d36c4ddc414fa927c6d527922616e4f2951ca87046", + "dependencies": [ + "WAWebPsGroupExitExperienceExitDeleteConfirmationDialogUiInteractionWamEvent", + "WAWebPsGroupExitExperienceExitDialogInteractionWamEvent", + "WAWebPsGroupExitExperienceGroupActionWamEvent", + "WAWebSpamConstants", + "WAWebWamEnumGroupExitExperienceOrigin", + "WAWebWamEnumPsGroupExitExperienceActions", + "WAWebWamEnumPsGroupExitExperienceDeleteConfirmationDialogActions", + "WAWebWamEnumPsGroupExitExperienceExitDialogActions" + ], + "end": 68715, + "factorySha256": "fe9058df08fabf78e5110335b0b938b713abc6a647c7f7699fb867530a16b0a6", + "moduleSha256": "7a7c86020d8023d7ac43439513a7d303fd0f0acfbe037114c6c255ff9c57166c", + "start": 63846 + } + ], + "WAWebInteractiveMessageCarouselLogEvents": [ + { + "bundleSha256": "42d8fdeb3a308f2dd813f71ee1f8cb04490c5c14b824926db713d8849b52d046", + "dependencies": [ + "WALogger", + "WAWebInteractiveMessageHeaderMediaType", + "WAWebMsgCollection", + "WAWebPaidMessagingUserInteractionsLoggerWamEvent", + "WAWebWamEnumPaidMessagingUserInteractionsActionTarget", + "WAWebWamEnumPaidMessagingUserInteractionsActionType", + "WAWebWamEnumPaidMessagingUserInteractionsComponentType", + "WAWebWamEnumPaidMessagingUserInteractionsHeaderMediaType", + "WAWebWamEnumPaidMessagingUserInteractionsMarketingFormat" + ], + "end": 194530, + "factorySha256": "8b18a15fe4f431036d89b0e5444aae58df52cc7783e56e717c986b77816ff332", + "moduleSha256": "1e7a9a8e67c3ea5e9f173c51ee16458f3181862346d90cef938458114bdcc2d7", + "start": 189040 + }, + { + "bundleSha256": "9b681e073f14f9d50e3dc12b456e4f980b554eff4d58827bc91c90a697e9ca38", + "dependencies": [ + "WALogger", + "WAWebInteractiveMessageHeaderMediaType", + "WAWebMsgCollection", + "WAWebPaidMessagingUserInteractionsLoggerWamEvent", + "WAWebWamEnumPaidMessagingUserInteractionsActionTarget", + "WAWebWamEnumPaidMessagingUserInteractionsActionType", + "WAWebWamEnumPaidMessagingUserInteractionsComponentType", + "WAWebWamEnumPaidMessagingUserInteractionsHeaderMediaType", + "WAWebWamEnumPaidMessagingUserInteractionsMarketingFormat" + ], + "end": 4140335, + "factorySha256": "8b18a15fe4f431036d89b0e5444aae58df52cc7783e56e717c986b77816ff332", + "moduleSha256": "1e7a9a8e67c3ea5e9f173c51ee16458f3181862346d90cef938458114bdcc2d7", + "start": 4134845 + } + ], "WAWebKeepInChatMsgAction": [ { "bundleSha256": "9b681e073f14f9d50e3dc12b456e4f980b554eff4d58827bc91c90a697e9ca38", @@ -1543,6 +1873,29 @@ "start": 211383 } ], + "WAWebLogStatusReply": [ + { + "bundleSha256": "7a224a1f4e67acb7e519b9676abfa86806e50426e7ed4a9f76e517536fe279ca", + "dependencies": [ + "WAWebConnGetters", + "WAWebConnModel", + "WAWebFrontendContactGetters", + "WAWebGroupMetadataCollection", + "WAWebMsgGetters", + "WAWebStatusLoggingUtils", + "WAWebStatusReplyWamEvent", + "WAWebUnifiedSession", + "WAWebWamEnumReplyEntryMethod", + "WAWebWamEnumStatusCategory", + "WAWebWamEnumStatusPosterContactType", + "asyncToGeneratorRuntime" + ], + "end": 100073, + "factorySha256": "5706dcf44b2875fc3bb5872b83a2922e6b8a20f7174a35425db232d361c8380b", + "moduleSha256": "a21ba878b42be9dc32114cb6f18c72a1f80dece8f46622142f5d07ad01d62151", + "start": 98095 + } + ], "WAWebMexLogging": [ { "bundleSha256": "430cb8979511aada95178605faa35a8483446f677ce6d1fe0fd66333c7879eee", @@ -1580,6 +1933,26 @@ "start": 486221 } ], + "WAWebOutContactLoggingUtils": [ + { + "bundleSha256": "65b89bd62b933ca76911900c83c40c2fad84263a99d2b99eced9578c1e4bdf55", + "dependencies": [ + "WAWebCompanionInviteContactWamEvent", + "WAWebContactCollection", + "WAWebOutContactCollection", + "WAWebOutContactInviteJourney", + "WAWebOutContactInviteUtils", + "WAWebWamEnumCompanionInviteActionType", + "WAWebWamEnumCompanionInviteMethodType", + "WAWebWamEnumCompanionInviteOriginType", + "asyncToGeneratorRuntime" + ], + "end": 109773, + "factorySha256": "11d3c6bb1bc65e80af4542d2e00f8b1b27bf0cb87f89aea6bd2ab5702310ab39", + "moduleSha256": "9432c365f05a84032454be3b7c10d57ff5618cd8175f1343b7bf1c5c883eea47", + "start": 106532 + } + ], "WAWebPaymentsUserActionLogging": [ { "bundleSha256": "b6a60494b96941d4f130f165112647667426337cb4060c63cfeb38330fb38bf1", @@ -1745,6 +2118,20 @@ "start": 920769 } ], + "WAWebSmbQpCallHealthLogger": [ + { + "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", + "dependencies": [ + "WAWebSmbQpCallHealthWamEvent", + "WAWebWamEnumFetchMechanismEnum", + "WAWebWamEnumFetchResultEnum" + ], + "end": 1822299, + "factorySha256": "44889e0ea5a53becf02caadda4b55f7e08c1b4d8eb0db920ccdc32180d5066b0", + "moduleSha256": "052f116f9ae291854880570c31095ba8962de655517aad5de3efc0efc39bd4e7", + "start": 1821678 + } + ], "WAWebStatusSubtitle.react": [ { "bundleSha256": "7a224a1f4e67acb7e519b9676abfa86806e50426e7ed4a9f76e517536fe279ca", @@ -1953,6 +2340,32 @@ "start": 1821158 } ], + "WAWebWamMemoryStat": [ + { + "bundleSha256": "082e0ef2faaa6247e6e17a225bb1685b7e583e946f7b0dc17ee58c431c1a07a8", + "dependencies": [ + "Promise", + "WALogger", + "WAShiftTimer", + "WAWebAppTracker", + "WAWebBackendApi", + "WAWebEnvironment", + "WAWebLidAwareContactsDB", + "WAWebMemoryStatWamEvent", + "WAWebRuntimeEnvironmentUtils", + "WAWebSchemaChat", + "WAWebSchemaMessage", + "WAWebWamEnumWebcScenarioType", + "WAWebWebcMemoryStatWamEvent", + "asyncToGeneratorRuntime", + "nullthrows" + ], + "end": 333425, + "factorySha256": "d9847636ca73ff7a075db6fb43f143ae2e39b7705b75f03ec4a2c2cfd4cb956a", + "moduleSha256": "40fef991a58d5c9f5b987e9436c41e50e26daa592c2c2f0cf4af0c218452aca7", + "start": 328387 + } + ], "WAWebWamTypeHash": [ { "bundleSha256": "09d91fdc4089c50ab1d35dd6c796ee82ef4c3296710316d20383c85738b871b8", @@ -2099,6 +2512,8 @@ "EBMessageMetadataQueryQuery.graphql", "EBMessageMetadataQueryQuery_facebookRelayOperation", "WAWebAccountLinkingAPIGetCertsQuery.graphql", + "WAWebAttachmentMenuLogger", + "WAWebChatMuteLogger", "WAWebContactManagerCustomerProfilesQuery", "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation", "WAWebCreateLabyrinthBackupJob", @@ -2107,12 +2522,17 @@ "WAWebDebugLabyrinthInboxSnapshotQuery.graphql", "WAWebDebugLabyrinthRangeQuery", "WAWebDebugLabyrinthRangeQuery.graphql", + "WAWebEditFormLogEvents", "WAWebFilterLogging", + "WAWebGroupExitModalLoggingUtils", + "WAWebInteractiveMessageCarouselLogEvents", "WAWebKeepInChatMsgAction", "WAWebKmpSyncdRequestBuilder", + "WAWebLogStatusReply", "WAWebMexLogging", "WAWebMexUpdateNewsletterAdminProfileSettingJob", "WAWebMexUpdateNewsletterAdminProfileSettingJobMutation.graphql", + "WAWebOutContactLoggingUtils", "WAWebPaymentsUserActionLogging", "WAWebPixWamLogger", "WAWebRotateLabyrinthEpochJob", @@ -2122,6 +2542,7 @@ "WAWebSetPrivacyJob", "WAWebSmb3pdAggregatedConversionSignalAction", "WAWebSmbMarkAsXOrderAction", + "WAWebSmbQpCallHealthLogger", "WAWebStatusSubtitle.react", "WAWebStatusViewerActionWamEvent", "WAWebTeamLinkCreateInvitationDialog.react", @@ -2145,6 +2566,7 @@ "WAWebUploadLabyrinthMessagesJobMutation.graphql", "WAWebWamCodegenUtils", "WAWebWamFingerprintReporter", + "WAWebWamMemoryStat", "WAWebWamTypeHash", "WAWebWindowsHybridBridgeWam.v2589", "WAWebWindowsHybridBridgeWam.v2590", diff --git a/generated/manifest.json b/generated/manifest.json index 2ee691aa..429787e0 100644 --- a/generated/manifest.json +++ b/generated/manifest.json @@ -88,9 +88,9 @@ "types": 27 }, "wam": { - "callSiteFieldValues": 804, - "callSiteFields": 3478, - "callSites": 813, + "callSiteFieldValues": 769, + "callSiteFields": 3453, + "callSites": 807, "constants": 6, "constructions": 874, "dropsByReason": { @@ -99,10 +99,10 @@ "unreadConstructionArgument.identifier": 75, "unreadConstructionArgument.member": 3 }, - "duplicateCallSites": 32, + "duplicateCallSites": 38, "events": 439, "globals": 46, - "partialCallSites": 113, + "partialCallSites": 107, "privateStatsIds": 9, "samplingWeightOverrides": 4 } @@ -165,7 +165,7 @@ "wam": { "file": "wam/index.json", "schema": "schema/wam.schema.json", - "sha256": "9c6b0dd3069c3d84b6bedd6223f502b4bfa36be236340b4a08f2c2ed61483bec" + "sha256": "01e97057758f1a148e8c9f6613d7de9b061410df863cc671aaf9dded69a2e248" }, "wasm": { "file": "wasm/index.json", diff --git a/generated/wam/index.json b/generated/wam/index.json index 84d60b18..4e115696 100644 --- a/generated/wam/index.json +++ b/generated/wam/index.json @@ -20549,12 +20549,7 @@ }, { "name": "replyEntryMethod", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumReplyEntryMethod", - "key": "TAP_REPLY_BAR" - } + "write": "constructor" }, { "name": "statusCategory", @@ -20704,33 +20699,6 @@ "WAWebWamMemoryStat" ], "callSites": [ - { - "module": "WAWebWamMemoryStat", - "fields": [ - { - "name": "isForeground", - "write": "constructor" - }, - { - "name": "peakUsedJsHeapSize", - "write": "constructor" - }, - { - "name": "scenario", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumWebcScenarioType", - "key": "CHAT_NAVIGATION" - } - }, - { - "name": "usedJsHeapSize", - "write": "constructor" - } - ], - "partial": true - }, { "module": "WAWebWamMemoryStat", "fields": [ @@ -21495,56 +21463,14 @@ "fields": [ { "name": "businessProfileEntryPoint", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumBusinessProfileEntryPoint", - "key": "SETTINGS" - } - }, - { - "name": "editBusinessProfileSessionId", "write": "constructor" }, - { - "name": "editProfileAction", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumEditProfileAction", - "key": "ACTION_OPEN" - } - } - ], - "partial": true - }, - { - "module": "WAWebEditFormLogEvents", - "fields": [ - { - "name": "businessProfileEntryPoint", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumBusinessProfileEntryPoint", - "key": "SETTINGS" - } - }, { "name": "editBusinessProfileSessionId", "write": "constructor" }, { "name": "editProfileAction", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumEditProfileAction", - "key": "ACTION_PROFILE_FIELD_OPEN" - } - }, - { - "name": "editProfileActionField", "write": "constructor" } ], @@ -21555,57 +21481,15 @@ "fields": [ { "name": "businessProfileEntryPoint", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumBusinessProfileEntryPoint", - "key": "SETTINGS" - } - }, - { - "name": "editBusinessProfileSessionId", "write": "constructor" }, - { - "name": "editProfileAction", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumEditProfileAction", - "key": "ACTION_PROFILE_FIELD_SAVE" - } - }, - { - "name": "editProfileActionField", - "write": "constructor" - } - ], - "partial": true - }, - { - "module": "WAWebEditFormLogEvents", - "fields": [ - { - "name": "businessProfileEntryPoint", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumBusinessProfileEntryPoint", - "key": "SETTINGS" - } - }, { "name": "editBusinessProfileSessionId", "write": "constructor" }, { "name": "editProfileAction", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumEditProfileAction", - "key": "ACTION_PROFILE_FIELD_DISCARD" - } + "write": "constructor" }, { "name": "editProfileActionField", @@ -30557,12 +30441,7 @@ "fields": [ { "name": "actionConducted", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumActionConducted", - "key": "MUTE" - } + "write": "constructor" }, { "name": "muteChatType", @@ -30584,12 +30463,7 @@ "fields": [ { "name": "actionConducted", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumActionConducted", - "key": "UNMUTE" - } + "write": "constructor" }, { "name": "muteChatType", @@ -46629,12 +46503,7 @@ }, { "name": "attachmentTrayAction", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumAttachmentTrayActionType", - "key": "CLICK" - } + "write": "constructor" }, { "name": "attachmentTrayActionTarget", @@ -47712,11 +47581,7 @@ "fields": [ { "name": "ctwaOrderSignalVersion", - "write": "constructor", - "value": { - "kind": "int", - "value": 1 - } + "write": "constructor" }, { "name": "customerAdsSharingSettingEnabled", @@ -47732,68 +47597,11 @@ }, { "name": "orderSignalType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumOrderSignalType", - "key": "UPDATED" - } - }, - { - "name": "orderStatus", "write": "constructor" }, - { - "name": "threadIdHmac", - "write": "constructor" - } - ], - "partial": true - }, - { - "module": "WAWebSmbMarkAsXOrderAction", - "fields": [ - { - "name": "ctwaOrderSignalVersion", - "write": "constructor", - "value": { - "kind": "int", - "value": 1 - } - }, - { - "name": "customerAdsSharingSettingEnabled", - "write": "constructor" - }, - { - "name": "deepLinkConversionSource", - "write": "constructor" - }, - { - "name": "orderPaid", - "write": "constructor", - "value": { - "kind": "bool", - "value": false - } - }, - { - "name": "orderSignalType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumOrderSignalType", - "key": "CREATED" - } - }, { "name": "orderStatus", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumOrderStatus", - "key": "PROCESSING" - } + "write": "constructor" }, { "name": "threadIdHmac", @@ -50815,72 +50623,6 @@ "WAWebInteractiveMessageCarouselLogEvents" ], "callSites": [ - { - "module": "WAWebInteractiveMessageCarouselLogEvents", - "fields": [ - { - "name": "pmxActionTarget", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumPaidMessagingUserInteractionsActionTarget", - "key": "THUMBNAIL" - } - }, - { - "name": "pmxActionType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumPaidMessagingUserInteractionsActionType", - "key": "CLICK" - } - }, - { - "name": "pmxComponentType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumPaidMessagingUserInteractionsComponentType", - "key": "HEADER" - } - } - ], - "partial": true - }, - { - "module": "WAWebInteractiveMessageCarouselLogEvents", - "fields": [ - { - "name": "pmxActionTarget", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumPaidMessagingUserInteractionsActionTarget", - "key": "HEADER" - } - }, - { - "name": "pmxActionType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumPaidMessagingUserInteractionsActionType", - "key": "CLICK" - } - }, - { - "name": "pmxComponentType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumPaidMessagingUserInteractionsComponentType", - "key": "HEADER" - } - } - ], - "partial": true - }, { "module": "WAWebInteractiveMessageCarouselLogEvents", "fields": [ @@ -50890,21 +50632,11 @@ }, { "name": "pmxActionType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumPaidMessagingUserInteractionsActionType", - "key": "CLICK" - } + "write": "constructor" }, { "name": "pmxComponentType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumPaidMessagingUserInteractionsComponentType", - "key": "BUTTON" - } + "write": "constructor" } ], "partial": true @@ -59378,11 +59110,7 @@ }, { "name": "psGroupExitExperienceEnabled", - "write": "constructor", - "value": { - "kind": "bool", - "value": true - } + "write": "constructor" }, { "name": "psGroupExitExperienceExitDialogAction", @@ -63868,12 +63596,7 @@ "fields": [ { "name": "fetchMechanism", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumFetchMechanismEnum", - "key": "GRAPHQL" - } + "write": "constructor" }, { "name": "fetchResult", @@ -71142,21 +70865,11 @@ "fields": [ { "name": "companionInviteAction", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumCompanionInviteActionType", - "key": "IMPRESSION" - } + "write": "constructor" }, { "name": "companionInviteMethod", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumCompanionInviteMethodType", - "key": "NATIVE_SMS" - } + "write": "constructor" }, { "name": "companionInviteOrigin", @@ -71170,21 +70883,11 @@ "fields": [ { "name": "companionInviteAction", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumCompanionInviteActionType", - "key": "INVITE_SEND" - } + "write": "constructor" }, { "name": "companionInviteMethod", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumCompanionInviteMethodType", - "key": "NATIVE_SMS" - } + "write": "constructor" }, { "name": "companionInviteOrigin", @@ -71202,21 +70905,11 @@ "fields": [ { "name": "companionInviteAction", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumCompanionInviteActionType", - "key": "INVITE_SEND" - } + "write": "constructor" }, { "name": "companionInviteMethod", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumCompanionInviteMethodType", - "key": "NATIVE_SMS" - } + "write": "constructor" }, { "name": "companionInviteOrigin", From da32bc8a62885d708d616c526fe46ff33652805e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:34:37 +0000 Subject: [PATCH 32/52] fix(wam): preserve object property overwrite order --- crates/wa-wam/src/callsites.rs | 38 ++++++++++++++++++++--------- crates/wa-wam/src/tests.rs | 35 ++++++++++++++++++++++++++ docs/snapshot-quality-2026-10-07.md | 4 +++ 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/crates/wa-wam/src/callsites.rs b/crates/wa-wam/src/callsites.rs index 1bcf19a6..1674a44d 100644 --- a/crates/wa-wam/src/callsites.rs +++ b/crates/wa-wam/src/callsites.rs @@ -383,17 +383,31 @@ fn read_argument( match prop { ObjectPropertyKind::ObjectProperty(p) => { match wa_oxc::property_key_name(&p.key) { - Some(name) => fields.push(( - name.to_string(), - WamFieldWrite::Constructor, - literal_value(&p.value, aliases), - )), - // A computed key writes a field whose name is a runtime value. - None => *partial = true, + Some(name) => { + let value = literal_value(&p.value, aliases); + for (previous_name, _, previous_value) in fields.iter_mut() { + if previous_name == name { + *previous_value = value.clone(); + } + } + fields.push((name.to_string(), WamFieldWrite::Constructor, value)); + } + // An unknown key may overwrite any preceding property. + None => { + for (_, _, value) in fields.iter_mut() { + *value = None; + } + *partial = true; + } } } - // A spread merges keys from elsewhere. - ObjectPropertyKind::SpreadProperty(_) => *partial = true, + // Preserve order: later explicit properties re-establish values. + ObjectPropertyKind::SpreadProperty(_) => { + for (_, _, value) in fields.iter_mut() { + *value = None; + } + *partial = true; + } } } return; @@ -418,9 +432,9 @@ fn read_argument( locals, ); if operand_partial { - // An unresolved key or spread can overwrite any earlier value, - // including a value within this operand. - for (_, _, value) in fields.iter_mut().chain(operand.iter_mut()) { + // Unresolved keys can overwrite preceding operands. Values + // within this operand already respect its property order. + for (_, _, value) in fields.iter_mut() { *value = None; } *partial = true; diff --git a/crates/wa-wam/src/tests.rs b/crates/wa-wam/src/tests.rs index a1d8a0ad..800dcd04 100644 --- a/crates/wa-wam/src/tests.rs +++ b/crates/wa-wam/src/tests.rs @@ -954,3 +954,38 @@ fn constructor_merges_follow_operand_order_without_overstating_unknown_overrides ); } } + +#[test] +fn object_properties_preserve_only_values_after_the_last_unknown_write() { + for (object, expected) in [ + ("{retryCount:3,...runtimeFields}", None), + ("{...runtimeFields,retryCount:4}", Some(4)), + ("{retryCount:3,[runtimeKey]:7}", None), + ("{[runtimeKey]:7,retryCount:4}", Some(4)), + ("{retryCount:3,...runtimeFields,retryCount:4}", Some(4)), + ] { + for argument in [ + "fields".to_string(), + object.to_string(), + "babelHelpers.extends({},fields)".to_string(), + format!("babelHelpers.extends({{}},{object})"), + ] { + let source = format!( + "{LOCAL_ARGUMENT_CATALOG}__d(\"Reporter\",[\"WAWebMessageSendWamEvent\"],function(){{function send(){{var fields={object};new(o(\"WAWebMessageSendWamEvent\")).MessageSendWamEvent({argument});}}}});" + ); + let (ir, _) = run_full(&source); + let site = &ir + .events + .iter() + .find(|e| e.name == "MessageSend") + .unwrap() + .call_sites[0]; + assert!(site.partial, "{object} / {argument}"); + assert_eq!( + site.fields[0].value, + expected.map(|value| WamCallSiteValue::Int { value }), + "{object} / {argument}" + ); + } + } +} diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index a39ddcfa..c61f82ba 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -161,6 +161,10 @@ operands now follow overwrite order; later event writes retain their existing conservative treatment. An unread later operand may overwrite earlier values, so those values are withdrawn until its keys can be established. Seven fixture cases cover both operand orders, unknown calls, computed keys and spreads. +A further 20-case regression checks property order within direct/local objects +and merge operands: unknown writes invalidate only preceding values; explicit +properties after a spread or computed key establish their final values. This +fix avoids both stale local values and unnecessary loss of later literals. On the exact current bundle set, this correction changes ten module/event pairs. Ignoring values, their field/write shapes are identical before and after; From d067d60b4042455aa9fb1c0fa4850b7e93fff214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:44:10 +0000 Subject: [PATCH 33/52] fix(wam): invalidate constants after opaque setters --- crates/wa-wam/src/callsites.rs | 32 +++++++----- crates/wa-wam/src/tests.rs | 40 +++++++++++++++ docs/snapshot-quality-2026-10-07.md | 17 +++++-- docs/snapshot-quality-2026-10-07.sources.json | 49 +++++++++++++++++++ generated/manifest.json | 4 +- generated/wam/index.json | 20 ++------ 6 files changed, 127 insertions(+), 35 deletions(-) diff --git a/crates/wa-wam/src/callsites.rs b/crates/wa-wam/src/callsites.rs index 1674a44d..529b3a50 100644 --- a/crates/wa-wam/src/callsites.rs +++ b/crates/wa-wam/src/callsites.rs @@ -123,9 +123,14 @@ pub(crate) fn scan_module(slice: &str) -> Vec { }; match w.field { Some(field) => site.fields.push((field, WamFieldWrite::Assigned, w.value)), - // A key the scan could not read still writes a field, so the site's list - // stops being the whole of what it writes. - None => site.partial = true, + // An opaque setter can overwrite any preceding value, even though + // the names already observed remain useful lower-bound evidence. + None => { + for (_, _, value) in &mut site.fields { + *value = None; + } + site.partial = true; + } } } sites @@ -303,24 +308,27 @@ impl<'a> Visit<'a> for SiteVisitor<'_> { None, ); let declared_in = binding_name(&base).and_then(|n| self.resolve(n)); - for (field, _, value) in fields { + // One unnamed write stands for everything the argument writes and the scan + // could not name, whether that is a spread, a computed key, or an object + // assembled somewhere else entirely. + if partial || unread.is_some() { self.writes.push(RawWrite { binding: base.clone(), - field: Some(field), - value, + field: None, + value: None, start: call.span.start, scope: self.scope_chain(), declared_in, }); } - // One unnamed write stands for everything the argument writes and the scan - // could not name, whether that is a spread, a computed key, or an object - // assembled somewhere else entirely. - if partial || unread.is_some() { + // read_argument already accounts for unknown writes within this + // operand. Apply its remaining known values after invalidating earlier + // event values; later event writes still merge conservatively. + for (field, _, value) in fields { self.writes.push(RawWrite { binding: base.clone(), - field: None, - value: None, + field: Some(field), + value, start: call.span.start, scope: self.scope_chain(), declared_in, diff --git a/crates/wa-wam/src/tests.rs b/crates/wa-wam/src/tests.rs index 800dcd04..57944428 100644 --- a/crates/wa-wam/src/tests.rs +++ b/crates/wa-wam/src/tests.rs @@ -989,3 +989,43 @@ fn object_properties_preserve_only_values_after_the_last_unknown_write() { } } } + +#[test] +fn opaque_post_constructor_writes_invalidate_preceding_constants() { + for write in [ + "e.set(runtimeFields)", + "e.set({[runtimeKey]:7})", + "e.set({...runtimeFields,deviceCount:4})", + ] { + let source = format!( + "{LOCAL_ARGUMENT_CATALOG}__d(\"Reporter\",[\"WAWebMessageSendWamEvent\"],function(){{function send(){{var fields={{retryCount:3}};var e=new(o(\"WAWebMessageSendWamEvent\")).MessageSendWamEvent(fields);{write};}}}});" + ); + let (ir, _) = run_full(&source); + let site = &ir + .events + .iter() + .find(|e| e.name == "MessageSend") + .unwrap() + .call_sites[0]; + assert!(site.partial, "{write}"); + assert_eq!( + site.fields + .iter() + .find(|f| f.name == "retryCount") + .unwrap() + .value, + None, + "{write}" + ); + if write.contains("deviceCount") { + assert_eq!( + site.fields + .iter() + .find(|f| f.name == "deviceCount") + .unwrap() + .value, + Some(WamCallSiteValue::Int { value: 4 }) + ); + } + } +} diff --git a/docs/snapshot-quality-2026-10-07.md b/docs/snapshot-quality-2026-10-07.md index c61f82ba..5b628eaa 100644 --- a/docs/snapshot-quality-2026-10-07.md +++ b/docs/snapshot-quality-2026-10-07.md @@ -165,6 +165,15 @@ A further 20-case regression checks property order within direct/local objects and merge operands: unknown writes invalidate only preceding values; explicit properties after a spread or computed key establish their final values. This fix avoids both stale local values and unnecessary loss of later literals. +Opaque later `event.set(...)` writes also invalidate preceding constants while +retaining their known field names. A before/after regression covers unknown +objects, computed keys and a spread followed by a known setter field. This +prevents newly recovered local constants from surviving an unproven overwrite. +On the locked snapshot this withdraws three values (connectionType, networkStack +and overallMmsVersion) in `WAWebCreateMediaDownloadMetrics`, whose event receives +`set(babelHelpers.extends({},e.imageDimensions))`. Those operand keys are not +recovered. Its source is included in the focused sidecar; site, field and +construction counts remain unchanged, and all other generated domains are identical. On the exact current bundle set, this correction changes ten module/event pairs. Ignoring values, their field/write shapes are identical before and after; @@ -179,7 +188,7 @@ conditional objects and logical expressions. This is a conservative correction to extraction certainty, not an upstream removal. Resolving those operand key sets could recover values later; the current extractor does not prove them. -The final totals are 807 sites, 3,453 field occurrences, 769 values and 107 +The final totals are 807 sites, 3,453 field occurrences, 766 values and 107 partial sites. The write guard caught 813 → 807; an isolated generation and the field/write-shape comparison established the six deduplications before replacing the generated WAM/manifest. No guard, floor or diagnostic baseline was lowered. @@ -212,9 +221,9 @@ cargo test -p wa-transform -p wa-wam -p whatspec --all-features ``` Source indexes are generated on demand (~14.2 MB each), not committed wholesale. -The focused source and gap ledgers are ~104.6 KB and ~21.4 KB. Two real WAM fixtures +The focused source and gap ledgers are ~106.7 KB and ~21.4 KB. Two real WAM fixtures retain exact source slices, with bundle hashes and offsets in their headers. -The WAM document grows from 2,794,121 to 2,807,208 bytes by recovering existing +The WAM document grows from 2,794,121 to 2,806,768 bytes by recovering existing field semantics; optional provenance adds no per-field IR metadata. WAM gap evidence incurs extra parsing only when requested. At the initial local-object revision, regeneration checks reproduce all 27 artifacts for the repaired current snapshot and a separately regenerated old snapshot. Observed check times were @@ -230,7 +239,7 @@ occurrence, with the field name, rather than a counter without source evidence. Filtered source sidecars now explicitly record their requested module names; absence findings are limited to that selection. -The generic contract report contains 495 deltas (2,725,481 bytes), keeps ordered +The generic contract report contains 495 deltas (2,725,041 bytes), keeps ordered arrays and does not infer renames. The [review ledger](snapshot-quality-2026-10-07.reviews.json) classifies the privacy improvement and persisted-ID change; the remaining 493 deltas stay indeterminate until diff --git a/docs/snapshot-quality-2026-10-07.sources.json b/docs/snapshot-quality-2026-10-07.sources.json index 1c410304..0b1c4b31 100644 --- a/docs/snapshot-quality-2026-10-07.sources.json +++ b/docs/snapshot-quality-2026-10-07.sources.json @@ -167,6 +167,29 @@ "start": 241411 } ], + "WAWebCreateMediaDownloadMetrics": [ + { + "bundleSha256": "ffe69f61924eb23c1d65cb89a152e4ce35ee404bd52a7edcced9c037b236f490", + "dependencies": [ + "WAWebAppTracker", + "WAWebCoreActionsODS", + "WAWebMediaCryptoEligibilityUtils", + "WAWebMediaDownload2WamEvent", + "WAWebMmsDownloadUploadCrashLogger", + "WAWebMmsMediaTypes", + "WAWebWamEnumConnectionType", + "WAWebWamEnumMediaDownloadResultType", + "WAWebWamEnumNetworkStackType", + "WAWebWamGroupMetricCache", + "WAWebWamMediaMetricUtils", + "asyncToGeneratorRuntime" + ], + "end": 3715179, + "factorySha256": "28049201a39821e258bb82e57a46e2ead4bfc9c56d0864dc8a3dba8c27325396", + "moduleSha256": "b5983d980dc5ac9db3c9c9512bac87958a0f794d42b722761ea44a393f5e3bc9", + "start": 3710612 + } + ], "WAWebDebugLabyrinthInboxSnapshotQuery": [ { "bundleSha256": "0710b116f2402703bb29fda44359d8e171368f2390325229eb84207aa27e8f35", @@ -1436,6 +1459,7 @@ "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation", "WAWebCreateLabyrinthBackupJob", "WAWebCreateLabyrinthBackupJobMutation.graphql", + "WAWebCreateMediaDownloadMetrics", "WAWebDebugLabyrinthInboxSnapshotQuery", "WAWebDebugLabyrinthInboxSnapshotQuery.graphql", "WAWebDebugLabyrinthRangeQuery", @@ -1616,6 +1640,30 @@ "start": 252637 } ], + "WAWebCreateMediaDownloadMetrics": [ + { + "bundleSha256": "f588ee2f033b87e018a3be34a37ec1347d8b6b8700de539e6be31481b29c4690", + "dependencies": [ + "WAWebAppTracker", + "WAWebCoreActionsODS", + "WAWebExperienceIdWamFields", + "WAWebMediaCryptoEligibilityUtils", + "WAWebMediaDownload2WamEvent", + "WAWebMmsDownloadUploadCrashLogger", + "WAWebMmsMediaTypes", + "WAWebWamEnumConnectionType", + "WAWebWamEnumMediaDownloadResultType", + "WAWebWamEnumNetworkStackType", + "WAWebWamGroupMetricCache", + "WAWebWamMediaMetricUtils", + "asyncToGeneratorRuntime" + ], + "end": 2094305, + "factorySha256": "92ec9196bb03ec55ca551494cf9688ee3a1cc3c7feb431c9e878f4feb80e72c6", + "moduleSha256": "58e2dd068081ba0b7312da568963dde22059085eac557ae8b0c11a90bdc3f55a", + "start": 2089600 + } + ], "WAWebEditFormLogEvents": [ { "bundleSha256": "7783a694b3a8bc3a379585817de0e477c75729c73b21adbffbf060f5f0ac2860", @@ -2518,6 +2566,7 @@ "WAWebContactManagerCustomerProfilesQuery_facebookRelayOperation", "WAWebCreateLabyrinthBackupJob", "WAWebCreateLabyrinthBackupJobMutation.graphql", + "WAWebCreateMediaDownloadMetrics", "WAWebDebugLabyrinthInboxSnapshotQuery", "WAWebDebugLabyrinthInboxSnapshotQuery.graphql", "WAWebDebugLabyrinthRangeQuery", diff --git a/generated/manifest.json b/generated/manifest.json index 429787e0..83e51bc2 100644 --- a/generated/manifest.json +++ b/generated/manifest.json @@ -88,7 +88,7 @@ "types": 27 }, "wam": { - "callSiteFieldValues": 769, + "callSiteFieldValues": 766, "callSiteFields": 3453, "callSites": 807, "constants": 6, @@ -165,7 +165,7 @@ "wam": { "file": "wam/index.json", "schema": "schema/wam.schema.json", - "sha256": "01e97057758f1a148e8c9f6613d7de9b061410df863cc671aaf9dded69a2e248" + "sha256": "333a1f4d1f9581fc45266dabb4b8df90c66226c74f722c04135f94dfb4d727ee" }, "wasm": { "file": "wasm/index.json", diff --git a/generated/wam/index.json b/generated/wam/index.json index 4e115696..a7a13ee6 100644 --- a/generated/wam/index.json +++ b/generated/wam/index.json @@ -23572,12 +23572,7 @@ "fields": [ { "name": "connectionType", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumConnectionType", - "key": "HOSTNAME" - } + "write": "constructor" }, { "name": "deviceCount", @@ -23601,12 +23596,7 @@ }, { "name": "networkStack", - "write": "constructor", - "value": { - "kind": "enumMember", - "module": "WAWebWamEnumNetworkStackType", - "key": "NATIVE" - } + "write": "constructor" }, { "name": "overallAttemptCount", @@ -23654,11 +23644,7 @@ }, { "name": "overallMmsVersion", - "write": "constructor", - "value": { - "kind": "int", - "value": 4 - } + "write": "constructor" }, { "name": "overallRetryCount", From ae274e30ed31cbfbe1eb533f61eb4287f688fc08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 7 Oct 2026 23:54:00 +0000 Subject: [PATCH 34/52] test: qualify final provenance semantics and document pilot limits --- tests/conformance/compiled/README.md | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index 09b376ab..2257910e 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -279,3 +279,45 @@ Using exactly those bytes without a trailing newline yields setHash no implementation was duplicated in this workstream. The integrated CLI also passes the locked current-snapshot `--check`, reproducing all 27 committed artifacts after the WAM changes. + +## Final quality integration d067d60 + +Integrated #52 `d067d60b4042455aa9fb1c0fa4850b7e93fff214` without +conflicts. The delta retains conservative WAM values after opaque setters, +property overwrites and merge operands, and explicitly represents incomplete +provenance instead of implying verification. + +Independent CLI reproductions close the remaining review findings: + +- With identical artifact/manifest bytes and two different self-consistent locks + sharing one WA version, report v2 returns `sameInputs: null`, + `sameDeclaredInputs: false` and `inputBinding.status: unverified` on both sides. + Comparing the same snapshot also leaves actual input identity unknown. +- A changed artifact and a matching v2 review retain `inputBinding: unverified` + on the delta. Applying an assessment does not promote declared inputs into + verified inputs. Version-1 review evidence is rejected explicitly. +- The exact global-channel fixture recorded above now returns two total gaps and + two `unlocatedDropsByReason` gaps, with an empty site list. No location is + fabricated and no counted gap disappears. + +The final composition passes 1,671 workspace tests, zero failures/ignored tests, +including eight compiled independent groups per snapshot and the IQ runtime +suite. All-feature/all-target Clippy, formatting, 12/12 committed schemas, lint, +15 quality Python tests and five fixture tests pass. These checks use the final +quality head; older generation size/timing observations above remain historical. + +Residual limits do not block the two bounded IQ pilots: historical manifests +cannot prove which declared bundle set generated their artifacts; unreadable +WAM global channel lists have counted but unlocated gaps; incomplete dynamic +JavaScript semantics remain explicit diagnostics. This conformance harness uses +an in-memory adapter and static source evidence, so it does not certify an +external binary codec, server behavior, all IQ operations or complete MEX/WAM +coverage. Report v2 requires consumers of the optional diff/review format to +migrate; no schema version or downstream-client contract was silently changed. + +The final locked `--check` passes for all 27 committed artifacts. The additional +Greptile merge-order finding is covered by the integrated seven-case +`constructor_merges_follow_operand_order_without_overstating_unknown_overrides` +regression: later unknown operands clear earlier constants while later explicit +writes can restore a known value. Its object-property and opaque-setter neighbor +regressions also pass. No further implementation was added in this workstream. From 0a7a4dda92e6ca98c02d2ab2fcf4aaa0662b29d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 14:42:28 +0000 Subject: [PATCH 35/52] ci: run snapshot validation regression tests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57fef1c7..bd5a3d8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,9 @@ jobs: pip install --quiet jsonschema python scripts/validate-schemas.py generated + - name: Test snapshot validation and reviewed baselines + run: python3 -m unittest discover -s scripts/tests -v + # The schemas check SHAPE; this checks that the document is usable. A # consumer generating code from the IR — in any language — has to trust # that an enum names its values, a byte pin matches its length, a union From c9eabb449573ae027fb0f7d4d6391768011392ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 14:46:23 +0000 Subject: [PATCH 36/52] fix: reject whole-file parser recovery before conformance capture --- .github/workflows/ci.yml | 3 ++ crates/wa-transform/examples/module_spans.rs | 47 +++++++++++++++++++- tests/conformance/compiled/README.md | 19 ++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55c1262b..7f69a14c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,9 @@ jobs: - name: Test run: cargo test --workspace + - name: Test conformance capture parser + run: cargo test -p wa-transform --example module_spans + # The JSON Schema derivation + envelope self-tests only build under the # `schema` feature, so run them explicitly. - name: Schema self-tests diff --git a/crates/wa-transform/examples/module_spans.rs b/crates/wa-transform/examples/module_spans.rs index 743bf477..e7520780 100644 --- a/crates/wa-transform/examples/module_spans.rs +++ b/crates/wa-transform/examples/module_spans.rs @@ -18,10 +18,55 @@ fn main() -> Result<()> { } else { std::fs::read_to_string(path)? }; - for module in wa_transform::extract_module_definitions(&source) { + for module in capture_modules(&source)? { if names.contains(&module.name) { println!("{}\t{}\t{}", module.name, module.start, module.end); } } Ok(()) } + +fn capture_modules(source: &str) -> Result> { + // Validate the complete file before emitting any span. The checked API also + // visits nested definitions; retain production boundaries for this capture. + wa_transform::extract_module_definitions_checked(source) + .map_err(|errors| anyhow::anyhow!("bundle parse failed: {}", errors.join("; ")))?; + Ok(wa_transform::extract_module_definitions(source)) +} + +#[cfg(test)] +mod tests { + use super::capture_modules; + + #[test] + fn malformed_file_cannot_qualify_complete_requested_modules() { + let valid = r#"__d("Wanted",[],function(){});"#; + for source in [ + format!("{valid} function broken("), + format!("function broken( {valid}"), + format!(r#"{valid} let = ; __d("Later",[],function(){{}});"#), + ] { + assert!( + capture_modules(&source).is_err(), + "accepted recovery: {source}" + ); + } + } + + #[test] + fn valid_capture_preserves_production_module_boundaries() { + let source = r#"__d('Outer',[],function(){__d('Inner',[],function(){});}); __d('Later',[],function(){});"#; + let captured = capture_modules(source).unwrap(); + assert_eq!(captured, wa_transform::extract_module_definitions(source)); + assert_eq!( + captured.iter().map(|m| m.name.as_str()).collect::>(), + ["Outer", "Later"] + ); + assert_eq!( + wa_transform::extract_module_definitions_checked(source) + .unwrap() + .len(), + 3 + ); + } +} diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index 2257910e..dc434afc 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -321,3 +321,22 @@ Greptile merge-order finding is covered by the integrated seven-case regression: later unknown operands clear earlier constants while later explicit writes can restore a known value. Its object-property and opaque-setter neighbor regressions also pass. No further implementation was added in this workstream. + +## Whole-file capture validation + +The module-spans capture now rejects parse errors anywhere in the complete +verified input before printing a span. It first uses the checked parser, then +uses the original extraction API to preserve outer-module boundaries. This adds +a validation parse per bundle; the shared extraction APIs and runtime handling +of unknown extensions are unchanged. + +The new example regression fails before the fix on a complete Wanted module +followed by `function broken(`, and passes afterward for errors before, between +and after valid definitions. A nested-definition regression preserves the same +outer spans as production extraction. CI explicitly runs these example tests. +The real CLI returns nonzero with empty stdout for the malformed tail. Locked +synthetic capture runs reject both same-file and later-file errors without +creating an output directory. All 66 preserved source fixtures still parse and +select their original named module; the five Python integrity tests, nine +wa-transform tests and targeted all-target Clippy pass. These checks do not claim +a new full bundle-store recapture after executor restart. From 11f02520cdae081c535a618763a1a3d6604a507d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 15:16:58 +0000 Subject: [PATCH 37/52] test: validate baseline capture on merged quality base --- .github/workflows/ci.yml | 3 ++ crates/wa-transform/examples/module_spans.rs | 47 +++++++++++++++++++- tests/conformance/README.md | 10 +++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55c1262b..7f69a14c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,9 @@ jobs: - name: Test run: cargo test --workspace + - name: Test conformance capture parser + run: cargo test -p wa-transform --example module_spans + # The JSON Schema derivation + envelope self-tests only build under the # `schema` feature, so run them explicitly. - name: Schema self-tests diff --git a/crates/wa-transform/examples/module_spans.rs b/crates/wa-transform/examples/module_spans.rs index 743bf477..e7520780 100644 --- a/crates/wa-transform/examples/module_spans.rs +++ b/crates/wa-transform/examples/module_spans.rs @@ -18,10 +18,55 @@ fn main() -> Result<()> { } else { std::fs::read_to_string(path)? }; - for module in wa_transform::extract_module_definitions(&source) { + for module in capture_modules(&source)? { if names.contains(&module.name) { println!("{}\t{}\t{}", module.name, module.start, module.end); } } Ok(()) } + +fn capture_modules(source: &str) -> Result> { + // Validate the complete file before emitting any span. The checked API also + // visits nested definitions; retain production boundaries for this capture. + wa_transform::extract_module_definitions_checked(source) + .map_err(|errors| anyhow::anyhow!("bundle parse failed: {}", errors.join("; ")))?; + Ok(wa_transform::extract_module_definitions(source)) +} + +#[cfg(test)] +mod tests { + use super::capture_modules; + + #[test] + fn malformed_file_cannot_qualify_complete_requested_modules() { + let valid = r#"__d("Wanted",[],function(){});"#; + for source in [ + format!("{valid} function broken("), + format!("function broken( {valid}"), + format!(r#"{valid} let = ; __d("Later",[],function(){{}});"#), + ] { + assert!( + capture_modules(&source).is_err(), + "accepted recovery: {source}" + ); + } + } + + #[test] + fn valid_capture_preserves_production_module_boundaries() { + let source = r#"__d('Outer',[],function(){__d('Inner',[],function(){});}); __d('Later',[],function(){});"#; + let captured = capture_modules(source).unwrap(); + assert_eq!(captured, wa_transform::extract_module_definitions(source)); + assert_eq!( + captured.iter().map(|m| m.name.as_str()).collect::>(), + ["Outer", "Later"] + ); + assert_eq!( + wa_transform::extract_module_definitions_checked(source) + .unwrap() + .len(), + 3 + ); + } +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md index 0025f866..d0c3e48a 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -191,3 +191,13 @@ no catalog entry 29 vs 41, and unread WAM arguments 93 vs 104. Baselines and CI were not changed here, apart from adding the independent fixture-test job. Published-SHA CI and combined emitter qualification are still required before declaring the repository qualified. + +## Post-quality integration + +Updated against main `9b8bc4e05eec14fa49b488c8eb8e56ce8ac5bfdb`, the +merged #52 quality work including the audit remediation. Exact lint baselines +remain enforced. The whole-file parse validation fix from #55 is included here +because this baseline owns the same capture helper: invalid input fails before +any span is emitted, while original outer-module boundaries are retained. CI +explicitly runs the helper's invalid-input and nested-boundary regressions. +This update does not depend on the IQ emitter or compiled qualification fixtures. From ee45fff1ecfa5c7239b346d812ac0c228e52fe9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 15:29:01 +0000 Subject: [PATCH 38/52] test: pin full capture and input provenance bindings --- tests/conformance/compiled/README.md | 17 +++++++++++++++++ tests/conformance/test_capture.py | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index dc434afc..c91640ef 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -340,3 +340,20 @@ creating an output directory. All 66 preserved source fixtures still parse and select their original named module; the five Python integrity tests, nine wa-transform tests and targeted all-target Clippy pass. These checks do not claim a new full bundle-store recapture after executor restart. + +## Complete provenance bindings + +The offline test now pins the complete bytes of all five provenance records: +two baseline captures, two compiled-source captures and selected-input origins. +This covers set hashes, bundle counts/bytes, every bundle hash and span, and the +selected inputs' commit/path/full source artifact hash. Metadata was checked +against historical committed locks and IQ artifacts before pinning; no complete +bundle recapture is claimed in this follow-up. Pins are reviewed anchors and are +not automatically regenerated from fixtures. They detect accidental drift, not +malicious coordinated changes to both evidence and tests. + +Twenty-six metadata mutations reproduce the gap: each passes the former test +and fails the pinned test, covering both snapshots and all record groups. +Unmodified evidence passes all six composition fixture tests. Updating a capture +now requires reviewing its complete provenance binding and deliberately updating +the corresponding pin, in addition to source/selected-input integrity checks. diff --git a/tests/conformance/test_capture.py b/tests/conformance/test_capture.py index ab0d887f..54b70e12 100644 --- a/tests/conformance/test_capture.py +++ b/tests/conformance/test_capture.py @@ -51,6 +51,28 @@ def test_single_quoted_and_escaped_names_reach_the_ast(self): with self.subTest(spelling=spelling): self.capture_synthetic(spelling) + def test_reviewed_provenance_bindings_are_pinned(self): + # These complete-record pins are reviewed anchors, not regenerated by + # capture.py. They include bundle identities/counts/offsets and, for + # selected inputs, the source commit/path/full artifact hash. Updating + # a capture requires reviewing these bindings before changing its pin. + pins = { + '2.3000.1045368834/provenance.json': + '41e4e797af7ab73dc7b3cd1e63e033b264c4bd6aa0bc6f7157262064ed78c13a', + '2.3000.1047483476/provenance.json': + '30f9ee277d45596ca423bbb2e24b513ee2a7ca220d01bce0978255cd19d7ffe8', + 'compiled/sources/2.3000.1045368834/provenance.json': + '1cab70d3e35f7d9ed3e0778c9b4999f9609ff63bba6edd47a0e14209fd5e4a60', + 'compiled/sources/2.3000.1047483476/provenance.json': + 'e295a819b5d9f6c55b99c1a8bc841d28756b6b18f244144729c746b893d0d811', + 'compiled/inputs/provenance.json': + '8f1f0fbdf79f4dacf3a251ffbcd0d6ae90fe013f1b790acba4cb3ce298f31d7b', + } + for relative, expected in pins.items(): + with self.subTest(provenance=relative): + raw = (Path(__file__).parent / relative).read_bytes() + self.assertEqual(hashlib.sha256(raw).hexdigest(), expected) + def test_committed_sources_match_every_recorded_span_hash(self): for base, modules in [(Path(__file__).parent, MODULES), (Path(__file__).parent / 'compiled/sources', COMPILED_MODULES)]: From 9356dee13ee2e4b36592287ebeb5a06cb0dce72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 15:29:01 +0000 Subject: [PATCH 39/52] test: pin reviewed baseline provenance records --- tests/conformance/README.md | 6 ++++++ tests/conformance/test_capture.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/conformance/README.md b/tests/conformance/README.md index d0c3e48a..531d6c4f 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -201,3 +201,9 @@ because this baseline owns the same capture helper: invalid input fails before any span is emitted, while original outer-module boundaries are retained. CI explicitly runs the helper's invalid-input and nested-boundary regressions. This update does not depend on the IQ emitter or compiled qualification fixtures. + +Complete provenance records are pinned separately from source-body hashes. +Changes to setHash, bundle counts/bytes, per-module bundle identity or offsets +must fail the offline check until deliberately reviewed. These pins were checked +against the recorded historical bundle locks; recapture does not update them +automatically. Hash pins detect drift, not authenticity of coordinated edits. diff --git a/tests/conformance/test_capture.py b/tests/conformance/test_capture.py index 59d97711..75ebcbf5 100644 --- a/tests/conformance/test_capture.py +++ b/tests/conformance/test_capture.py @@ -50,6 +50,22 @@ def test_single_quoted_and_escaped_names_reach_the_ast(self): with self.subTest(spelling=spelling): self.capture_synthetic(spelling) + def test_reviewed_provenance_bindings_are_pinned(self): + # These complete-record pins are reviewed anchors, not regenerated by + # capture.py. They include bundle identities/counts/offsets and, for + # selected inputs, the source commit/path/full artifact hash. Updating + # a capture requires reviewing these bindings before changing its pin. + pins = { + '2.3000.1045368834/provenance.json': + '41e4e797af7ab73dc7b3cd1e63e033b264c4bd6aa0bc6f7157262064ed78c13a', + '2.3000.1047483476/provenance.json': + '30f9ee277d45596ca423bbb2e24b513ee2a7ca220d01bce0978255cd19d7ffe8', + } + for relative, expected in pins.items(): + with self.subTest(provenance=relative): + raw = (Path(__file__).parent / relative).read_bytes() + self.assertEqual(hashlib.sha256(raw).hexdigest(), expected) + def test_committed_sources_match_every_recorded_span_hash(self): for version in ['2.3000.1045368834', '2.3000.1047483476']: root = Path(__file__).parent / version From 9de5d0ae8d8561b1036ce3bd875117f2cc152804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:01:08 -0300 Subject: [PATCH 40/52] fix(codegen): preserve IQ assertion semantics during admission --- crates/wa-codegen/src/spec.rs | 265 ++++++++++++++++++++++++++++++++-- docs/iq-response-admission.md | 16 +- 2 files changed, 264 insertions(+), 17 deletions(-) diff --git a/crates/wa-codegen/src/spec.rs b/crates/wa-codegen/src/spec.rs index 044144b8..bf48b515 100644 --- a/crates/wa-codegen/src/spec.rs +++ b/crates/wa-codegen/src/spec.rs @@ -182,8 +182,8 @@ fn fail_required_fields(fields: &[wa_ir::ParsedField]) -> std::collections::BTre /// but emits a diagnostic error instead of silently dropping outcomes. /// A value a response variant pins before it will accept a node. /// -/// Two kinds, because an outcome root has two things to pin: an ATTRIBUTE by name, and the -/// node's own text CONTENT, which `literalContent` writes and which has no name at all. +/// Root tag, named attribute and text content pins are shared between admission +/// and emitted selectors. `literalContent` has no attribute name. /// Modelling the set as `(name, value)` pairs kept the first and dropped the second, so a /// content-discriminated arm emitted no selector and took every response whose required fields /// happened to parse — whichever content value made the source parser select a later one. @@ -191,6 +191,7 @@ fn fail_required_fields(fields: &[wa_ir::ParsedField]) -> std::collections::BTre enum Pin<'a> { Attr(&'a str, &'a str), Content(&'a str), + Tag(&'a str), } /// The pins a response variant asserts — the set that has to pick this variant alone before a @@ -205,6 +206,7 @@ fn variant_pins(v: &ResponseVariant) -> Vec> { }, // `name` is unused for a content pin; the value is the whole of it. AssertionKind::Content => a.value.as_deref().map(Pin::Content), + AssertionKind::Tag => a.name.as_deref().map(Pin::Tag), _ => None, }) .collect() @@ -215,6 +217,7 @@ fn pin_conditions(v: &ResponseVariant, node: &str) -> Vec { let mut conditions: Vec = variant_pins(v) .into_iter() .map(|pin| match pin { + Pin::Tag(tag) => format!("{node}.tag == {}", rust_lit(tag)), Pin::Attr(name, value) => format!( "{node}.get_attr({}).map(|x| x.as_str()).as_deref() == Some({})", rust_lit(name), @@ -239,15 +242,6 @@ fn pin_conditions(v: &ResponseVariant, node: &str) -> Vec { ) }) })); - conditions.extend(v.assertions.iter().filter_map(|a| { - if a.kind == AssertionKind::Tag { - a.name - .as_ref() - .map(|name| format!("{node}.tag == {}", rust_lit(name))) - } else { - None - } - })); if let Some(error) = error_selection(v, node) { conditions.push(error); } @@ -269,11 +263,41 @@ fn pins_can_coincide(mine: &[Pin<'_>], other: &[Pin<'_>]) -> bool { // A node has ONE text content, so two variants pinning it to different values are // as exclusive as two disagreeing on an attribute. (Pin::Content(value), Pin::Content(o_value)) => value != o_value, + (Pin::Tag(value), Pin::Tag(o_value)) => value != o_value, _ => false, }) }) } +/// Every modeled earlier guard must follow from a later guard. Assertion order, +/// duplicates and metadata unused by that guard do not affect implication. +fn assertions_implied_by(earlier: &ResponseVariant, later: &ResponseVariant) -> bool { + earlier.assertions.iter().all(|a| { + later.assertions.iter().any(|b| { + if a.kind != b.kind { + return false; + } + match a.kind { + AssertionKind::Tag | AssertionKind::Child => a.name.is_some() && a.name == b.name, + AssertionKind::Attr => { + a.name.is_some() + && a.name == b.name + && (a.value.is_none() || a.value == b.value) + } + AssertionKind::Content => a.value.is_some() && a.value == b.value, + AssertionKind::Reference => { + a.name.is_some() + && a.name == b.name + && a.reference_path.is_some() + && a.reference_path == b.reference_path + } + // The direct outcome selector does not model this guard. + AssertionKind::FromServer => false, + } + }) + }) +} + /// Error predicates are sets of (integer code, optional text pin). Compare unions /// of intervals only; this does not model arbitrary parser control flow. fn error_arms_cover(earlier: &[wa_ir::ErrorArm], later: &[wa_ir::ErrorArm]) -> bool { @@ -305,7 +329,7 @@ fn error_arms_cover(earlier: &[wa_ir::ErrorArm], later: &[wa_ir::ErrorArm]) -> b /// The direct, bounded code/text error union observed in the pilots. Unknown /// payload structures use the ordinary admission path, never this specialization. -fn direct_error_payload(v: &ResponseVariant) -> Option<&wa_ir::ParsedField> { +fn direct_error_payload_shape(v: &ResponseVariant) -> Option<&wa_ir::ParsedField> { use wa_ir::ParsedFieldType; if v.error_arms.is_empty() || v.error_envelope.is_some() || v.fields.len() != 2 { return None; @@ -386,6 +410,31 @@ fn direct_error_payload(v: &ResponseVariant) -> Option<&wa_ir::ParsedField> { Some(payload) } +/// Admit the existing bounded shape only when every additional assertion is +/// enforced by its tag/code/text decoder. Other shapes keep ordinary admission. +fn direct_error_payload(v: &ResponseVariant) -> Option<&wa_ir::ParsedField> { + let payload = direct_error_payload_shape(v)?; + for (variant, arm) in payload.union_variants.as_ref()?.iter().zip(&v.error_arms) { + // This specialization reads only the error tag and required code/text + // attributes. Any other assertion needs a different supported parser; + // never silently lose it by selecting the arm from ErrorArm alone. + if variant.assertions.iter().any(|a| match a.kind { + AssertionKind::Tag => a.name.as_deref() != Some("error"), + AssertionKind::Attr => match a.name.as_deref() { + Some("code") => a.value.as_ref().is_some_and(|value| { + arm.code.is_none() || value.parse::().ok() != arm.code + }), + Some("text") => a.value.is_some() && a.value != arm.text, + _ => true, + }, + _ => true, + }) { + return None; + } + } + Some(payload) +} + fn error_arm_condition(arm: &wa_ir::ErrorArm) -> String { let mut terms = Vec::new(); if let Some(code) = arm.code { @@ -584,6 +633,13 @@ fn emit_outcome_types( names.push((vname.clone(), format!("{spec_base}{vname}"))); } for (index, (variant, (_, name))) in op.response.variants.iter().zip(&names).enumerate() { + if direct_error_payload_shape(variant).is_some() && direct_error_payload(variant).is_none() + { + return Err(format!( + "outcomes.error_contract_unsupported: response.variants[{index}] {} has an unmodeled direct error payload or guard", + variant.tag + )); + } if variant.fields.iter().any(|field| { field.field_type == wa_ir::ParsedFieldType::Union && crate::union::classify_union(field).is_none() @@ -648,7 +704,7 @@ fn emit_outcome_types( } // Several preceding error outcomes can collectively cover a later one even - // when no single outcome does. Only combine roots with the same assertions. + // when no single outcome does. A later response must imply the earlier guards. for j in 1..op.response.variants.len() { let later = &op.response.variants[j]; if error_selection(later, "response").is_none() { @@ -659,7 +715,7 @@ fn emit_outcome_types( .enumerate() .filter(|(i, earlier)| { req[*i].is_subset(&req[j]) - && earlier.assertions == later.assertions + && assertions_implied_by(earlier, later) && error_selection(earlier, "response").is_some() }) .flat_map(|(_, earlier)| earlier.error_arms.iter().cloned()) @@ -2500,6 +2556,187 @@ mod tests { ); } + fn bounded_error_variant(lo: i64, hi: i64) -> ResponseVariant { + let ir: wa_ir::IqIr = + serde_json::from_str(include_str!("../../../generated/iq/index.json")).unwrap(); + let op = ir + .stanzas + .iter() + .find(|o| o.module_name == "WASmaxOutGroupsSetSubjectRequest") + .unwrap(); + let mut v = op.response.variants[1].clone(); + v.tag = format!("Error{lo}To{hi}"); + v.assertions.retain(|a| a.kind != AssertionKind::Reference); + let arm = v.error_arms.last().unwrap().clone(); + let mut exact_arm = v.error_arms[0].clone(); + exact_arm.code = Some(lo); + v.error_arms = vec![ + exact_arm, + wa_ir::ErrorArm { + code_min: Some(lo), + code_max: Some(hi), + ..arm + }, + ]; + let payload = v + .fields + .iter_mut() + .find(|f| f.field_type == wa_ir::ParsedFieldType::Union) + .unwrap(); + let mut variant = payload + .union_variants + .as_ref() + .unwrap() + .last() + .unwrap() + .clone(); + let code = variant + .fields + .iter_mut() + .find(|f| f.name == "code") + .unwrap(); + code.int_min = Some(lo); + code.int_max = Some(hi); + let mut exact = payload.union_variants.as_ref().unwrap()[0].clone(); + exact + .fields + .iter_mut() + .find(|f| f.name == "code") + .unwrap() + .literal_value = Some(lo.to_string()); + exact + .assertions + .iter_mut() + .find(|a| a.name.as_deref() == Some("code")) + .unwrap() + .value = Some(lo.to_string()); + payload.union_variants = Some(vec![exact, variant]); + v + } + + #[test] + fn collective_error_coverage_ignores_assertion_order_and_extra_later_guards() { + for extra_later_guard in [false, true] { + let a = bounded_error_variant(400, 449); + let mut b = bounded_error_variant(450, 499); + let mut later = bounded_error_variant(400, 499); + b.assertions.reverse(); + if extra_later_guard { + later.assertions.push(wa_ir::ResponseAssertion { + kind: AssertionKind::Child, + name: Some("detail".into()), + value: None, + reference_path: None, + }); + } + let mut op = stanza("CollectiveErrors", None); + op.response.variants = vec![a, b, later]; + let mut emitted = Vec::new(); + let error = emit_outcome_types( + &op, + "Collective", + "CollectiveResponse", + "test", + &mut emitted, + ) + .unwrap_err(); + assert!(error.contains("preceding error outcomes cover"), "{error}"); + assert!(emitted.is_empty()); + // An earlier extra guard is not implied by the later response. Its + // interval must not count toward collective coverage. + op.response.variants[0] + .assertions + .push(wa_ir::ResponseAssertion { + kind: AssertionKind::Child, + name: Some("only-first".into()), + value: None, + reference_path: None, + }); + assert!( + emit_outcome_types( + &op, + "Collective", + "CollectiveResponse", + "test", + &mut emitted + ) + .is_ok() + ); + } + } + + #[test] + fn direct_error_extra_guards_are_rejected_not_omitted() { + for assertion in [ + wa_ir::ResponseAssertion { + kind: AssertionKind::Attr, + name: Some("scope".into()), + value: Some("group".into()), + reference_path: None, + }, + wa_ir::ResponseAssertion { + kind: AssertionKind::Child, + name: Some("proof".into()), + value: None, + reference_path: None, + }, + wa_ir::ResponseAssertion { + kind: AssertionKind::Tag, + name: Some("different".into()), + value: None, + reference_path: None, + }, + wa_ir::ResponseAssertion { + kind: AssertionKind::Attr, + name: Some("code".into()), + value: Some("401".into()), + reference_path: None, + }, + ] { + let mut v = bounded_error_variant(400, 499); + assert!(direct_error_payload(&v).is_some()); + let payload = v + .fields + .iter_mut() + .find(|f| f.field_type == wa_ir::ParsedFieldType::Union) + .unwrap(); + payload.union_variants.as_mut().unwrap()[0] + .assertions + .push(assertion); + assert!( + direct_error_payload(&v).is_none(), + "unmodeled guard must prevent specialization" + ); + let mut op = stanza("GuardedError", None); + op.response.variants = vec![v]; + let code = generate_spec(&op, "TEST_NAMESPACE", "GuardedErrorSpec"); + assert!( + code.contains("outcomes.error_contract_unsupported"), + "{code}" + ); + } + } + + #[test] + fn disjoint_root_tags_are_admitted_and_selected() { + let variant = |tag: &str| ResponseVariant { + tag: tag.into(), + assertions: vec![wa_ir::ResponseAssertion { + kind: AssertionKind::Tag, + name: Some(tag.into()), + value: None, + reference_path: None, + }], + ..Default::default() + }; + let mut op = stanza("DisjointTags", None); + op.response.variants = vec![variant("iq"), variant("message")]; + let code = generate_spec(&op, "TEST_NAMESPACE", "DisjointTagsSpec"); + assert!(!code.contains("RESPONSE_GENERATION_ERROR"), "{code}"); + assert!(code.contains("response.tag == \"iq\"")); + assert!(code.contains("response.tag == \"message\"")); + } + #[test] fn error_admission_preserves_text_pairs_ranges_and_fallback_order() { fn exact(code: i64, text: &str) -> wa_ir::ErrorArm { diff --git a/docs/iq-response-admission.md b/docs/iq-response-admission.md index 11717df8..c5717da2 100644 --- a/docs/iq-response-admission.md +++ b/docs/iq-response-admission.md @@ -22,6 +22,7 @@ certified complete. | `response.parser_unemittable` | The generated initializer cannot represent the recovered payload. | | `guards.reference_unsupported` / `guards.reference_nonuniform` | A recovered request reference cannot use the supported uniform wire context. | | `guards.request_context_required` | Call `parse_response_with_request` with the actual request ID and target. | +| `outcomes.error_contract_unsupported` | A direct error payload has an assertion outside the bounded tag/code/text decoder; no generic fallback may erase that guard. | | `outcomes.unemittable` | A variant parser cannot be emitted, or an earlier variant can shadow a later one under the emitter's known predicates. The message names variant indexes and tags. | An explicit, admitted empty outcome still produces a successful empty outcome @@ -70,6 +71,15 @@ for correlated operations. It must not fabricate context from the response. Unsupported reference paths fail admission explicitly. No transport, event or consumer API is added, and no IR/schema changes are needed. +Integration review added three generator regressions, all failing at +`09adb8b7809b85dba8fdde4d3ebd7a3a4d0aed1b` and passing after correction: +collective error-range coverage uses guard implication rather than assertion +vector order; extra unmodeled direct-error assertions reject specialization +with a diagnostic; and disjoint root tags participate in both admission and +runtime selection. A stricter earlier child guard is a negative control for +coverage. These constructed IR cases check generator invariants; they do not +claim new server behavior. The real pilot execution tests remain unchanged. + ## Verified pilots | Operation | Request evidence | Ordered outcomes | @@ -158,15 +168,15 @@ context API failure. The adapter does not qualify a binary codec or a downstream client; independent conformance remains separately owned. Two debug generations of all 142 IQ operations produced identical 2,061,750-byte -outputs, SHA-256 `006b5a5e392892b4cb89f98274bf3eed171d66179f752c83bb491ee387de3d71`, -in 3.03 and 3.00 seconds, with 39 explicitly rejected parsers. The original base +outputs, SHA-256 `214c9d9fede66c9ff6cf4194e75a506b8fd780b9c56675f0232f864626d6ecbd`, +in 4.43 and 3.92 seconds, with 39 explicitly rejected parsers. The original base produced 1,523,859 bytes in 2.84 and 2.98 seconds. The increase includes recovered outcome types and context-taking parsers; it is not an optimization claim. The Rust reference catalog remains ignored by git. Maintenance requires review of the admitted/rejected operation set and the bounded helper against source changes; neither a rejection count nor reduced output size is a quality target. -Reproduce with `cargo test --locked -p wa-codegen`, including 178 unit tests and +Reproduce with `cargo test --locked -p wa-codegen`, including 181 unit tests and the compiled runtime integration test; `cargo clippy --locked -p wa-codegen --all-targets -- -D warnings`; schema validation; and full bundle regeneration with `whatspec update --check`. No generated IR or baseline changes are included. From 4471cbc3bc1b07e71ef15627281ac149cd41e91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:37:34 -0300 Subject: [PATCH 41/52] fix(codegen): prove payload coverage and enforce response guards --- crates/wa-codegen/src/spec.rs | 387 ++++++++++++++++-- .../wa-codegen/tests/fixtures/iq_runtime.rs | 110 ++++- crates/wa-codegen/tests/iq_runtime.rs | 110 +++++ docs/iq-response-admission.md | 58 ++- 4 files changed, 618 insertions(+), 47 deletions(-) diff --git a/crates/wa-codegen/src/spec.rs b/crates/wa-codegen/src/spec.rs index bf48b515..526f1f2e 100644 --- a/crates/wa-codegen/src/spec.rs +++ b/crates/wa-codegen/src/spec.rs @@ -212,36 +212,49 @@ fn variant_pins(v: &ResponseVariant) -> Vec> { .collect() } -/// The same pins as generated conditions. -fn pin_conditions(v: &ResponseVariant, node: &str) -> Vec { - let mut conditions: Vec = variant_pins(v) +/// Model only guards whose wire semantics are present in the IR. References are +/// checked by the context entry point; an unlocated server predicate is not a +/// constant address and must never be guessed. +fn assertion_conditions( + assertions: &[wa_ir::ResponseAssertion], + node: &str, +) -> Result, String> { + let mut conditions = Vec::new(); + for a in assertions { + let condition = match a.kind { + AssertionKind::Tag => a.name.as_ref().map(|tag| format!("{node}.tag() == {}", rust_lit(tag))), + AssertionKind::Attr => a.name.as_ref().map(|name| match &a.value { + Some(value) => format!("{node}.get_attr({}).map(|x| x.as_str()).as_deref() == Some({})", rust_lit(name), rust_lit(value)), + None => format!("{node}.get_attr({}).is_some()", rust_lit(name)), + }), + AssertionKind::Content => a.value.as_ref().map(|value| format!("{node}.content_str().as_deref() == Some({})", rust_lit(value))), + AssertionKind::Child => a.name.as_ref().map(|tag| format!("({node}.content_bytes().is_none() && {node}.get_children_by_tag({}).count() == 1)", rust_lit(tag))), + AssertionKind::Reference => continue, + AssertionKind::FromServer => return Err("guards.from_server_unrecovered: server predicate has no recovered value".into()), + }; + conditions + .push(condition.ok_or_else(|| format!("guards.assertion_incomplete: {:?}", a.kind))?); + } + Ok(conditions) +} + +fn emit_response_guards(op: &IqStanzaDef, indent: &str) -> Vec { + assertion_conditions(&op.response.assertions, "response") + .expect("response guards were admitted") .into_iter() - .map(|pin| match pin { - Pin::Tag(tag) => format!("{node}.tag == {}", rust_lit(tag)), - Pin::Attr(name, value) => format!( - "{node}.get_attr({}).map(|x| x.as_str()).as_deref() == Some({})", - rust_lit(name), - rust_lit(value), - ), - Pin::Content(value) => format!( - "{node}.content_str().as_deref() == Some({})", - rust_lit(value), - ), + .map(|condition| { + format!( + "{indent}if !({condition}) {{ anyhow::bail!({}); }}", + rust_lit(&format!("{}: response guard failed", op.module_name)) + ) }) - .collect(); - // flattenedChildWithTag is a presence guard even when no payload is read. - // Keep it in both the admission signature and the emitted selector. - conditions.extend(v.assertions.iter().filter_map(|a| { - (a.kind == AssertionKind::Child) - .then_some(a.name.as_deref()) - .flatten() - .map(|name| { - format!( - "{node}.get_children_by_tag({}).count() == 1", - rust_lit(name) - ) - }) - })); + .collect() +} + +/// The same guard model serves selectors and single-shape response parsers. +fn pin_conditions(v: &ResponseVariant, node: &str) -> Vec { + let mut conditions = + assertion_conditions(&v.assertions, node).expect("outcome guards were admitted"); if let Some(error) = error_selection(v, node) { conditions.push(error); } @@ -272,7 +285,23 @@ fn pins_can_coincide(mine: &[Pin<'_>], other: &[Pin<'_>]) -> bool { /// Every modeled earlier guard must follow from a later guard. Assertion order, /// duplicates and metadata unused by that guard do not affect implication. fn assertions_implied_by(earlier: &ResponseVariant, later: &ResponseVariant) -> bool { + let bounded_error = direct_error_payload(later).is_some(); earlier.assertions.iter().all(|a| { + // The bounded decoder always enforces these field-derived predicates, + // even if the assertion mirror omits them. + if bounded_error + && match a.kind { + AssertionKind::Attr => { + a.name.as_deref() == Some("type") + && (a.value.is_none() || a.value.as_deref() == Some("error")) + } + AssertionKind::Child => a.name.as_deref() == Some("error"), + _ => false, + } + { + return true; + } + later.assertions.iter().any(|b| { if a.kind != b.kind { return false; @@ -327,6 +356,78 @@ fn error_arms_cover(earlier: &[wa_ir::ErrorArm], later: &[wa_ir::ErrorArm]) -> b }) } +/// Prove coverage using both code/text bands and the remaining payload checks. +/// Empty payload checks cover all later payloads. Identical optional-child +/// checks (or a subset) also suffice. An extra earlier child has a concrete +/// counterexample: duplicate that child, or use bytes when the later arm reads +/// no children. Other differing child contracts are explicitly unsupported. +fn error_outcomes_cover( + earlier: &[&ResponseVariant], + later: &ResponseVariant, +) -> Result { + let later_payload = direct_error_payload(later).expect("bounded later error"); + for (later_variant, later_arm) in later_payload + .union_variants + .as_ref() + .unwrap() + .iter() + .zip(&later.error_arms) + { + let later_children: Vec<_> = later_variant + .fields + .iter() + .filter(|f| f.method == "child") + .collect(); + let mut covering = Vec::new(); + for previous in earlier { + let payload = direct_error_payload(previous).expect("bounded earlier error"); + for (variant, arm) in payload + .union_variants + .as_ref() + .unwrap() + .iter() + .zip(&previous.error_arms) + { + let lo = arm.code.or(arm.code_min).unwrap(); + let hi = arm.code.or(arm.code_max).unwrap(); + let later_lo = later_arm.code.or(later_arm.code_min).unwrap(); + let later_hi = later_arm.code.or(later_arm.code_max).unwrap(); + if hi < later_lo + || later_hi < lo + || (arm.text.is_some() + && later_arm.text.is_some() + && arm.text != later_arm.text) + { + continue; + } + let children: Vec<_> = variant + .fields + .iter() + .filter(|f| f.method == "child") + .collect(); + if children.iter().all(|child| later_children.contains(child)) { + covering.push(arm.clone()); + } else if later_children.is_empty() + || children.iter().any(|child| { + !later_children.iter().any(|other| { + child.tag.as_deref().unwrap_or(&child.name) + == other.tag.as_deref().unwrap_or(&other.name) + }) + }) + { + // This earlier arm has extra failure modes; it cannot prove coverage. + } else { + return Err("outcomes.payload_coverage_unknown: differing optional-child contracts require a payload implication proof".into()); + } + } + } + if !error_arms_cover(&covering, std::slice::from_ref(later_arm)) { + return Ok(false); + } + } + Ok(true) +} + /// The direct, bounded code/text error union observed in the pilots. Unknown /// payload structures use the ordinary admission path, never this specialization. fn direct_error_payload_shape(v: &ResponseVariant) -> Option<&wa_ir::ParsedField> { @@ -414,7 +515,53 @@ fn direct_error_payload_shape(v: &ResponseVariant) -> Option<&wa_ir::ParsedField /// enforced by its tag/code/text decoder. Other shapes keep ordinary admission. fn direct_error_payload(v: &ResponseVariant) -> Option<&wa_ir::ParsedField> { let payload = direct_error_payload_shape(v)?; + let header = v + .fields + .iter() + .find(|f| f.field_type != wa_ir::ParsedFieldType::Union)?; + if header.field_type != wa_ir::ParsedFieldType::String + || header.source_path.is_some() + || header.enum_keys.is_some() + || header.enum_ref.is_some() + || header.pending_enum_ref.is_some() + || header.unknown_value.is_some() + || header.children.is_some() + || header.union_variants.is_some() + || header.byte_length.is_some() + || header.byte_min.is_some() + || header.byte_max.is_some() + || header.int_min.is_some() + || header.int_max.is_some() + { + return None; + } for (variant, arm) in payload.union_variants.as_ref()?.iter().zip(&v.error_arms) { + // Coverage and decoding both rely on code/text having no additional + // constraints or alternate locations outside this bounded model. + if variant + .fields + .iter() + .filter(|f| matches!(f.wire_name.as_deref().unwrap_or(&f.name), "code" | "text")) + .any(|f| { + f.source_path.is_some() + || f.children.is_some() + || f.union_variants.is_some() + || f.enum_keys.is_some() + || f.enum_ref.is_some() + || f.pending_enum_ref.is_some() + || f.unknown_value.is_some() + || f.byte_length.is_some() + || f.byte_min.is_some() + || f.byte_max.is_some() + || (f.wire_name.as_deref().unwrap_or(&f.name) == "text" + && (f.field_type != wa_ir::ParsedFieldType::String + || f.int_min.is_some() + || f.int_max.is_some())) + }) + { + return None; + } + // This specialization reads only the error tag and required code/text // attributes. Any other assertion needs a different supported parser; // never silently lose it by selecting the arm from ErrorArm alone. @@ -459,7 +606,7 @@ fn error_selection(v: &ResponseVariant, node: &str) -> Option { direct_error_payload(v)?; let arms: Vec<_> = v.error_arms.iter().map(error_arm_condition).collect(); Some(format!( - "({node}.get_children_by_tag(\"error\").count() == 1 && {node}.get_optional_child(\"error\").is_some_and(|error| error.get_attr(\"text\").is_some() && error.get_attr(\"code\").and_then(|v| __iq_error_code(v.as_str())).is_some_and(|code| {})))", + "({node}.get_attr(\"type\").map(|v| v.as_str()).as_deref() == Some(\"error\") && {node}.content_bytes().is_none() && {node}.get_children_by_tag(\"error\").count() == 1 && {node}.get_optional_child(\"error\").is_some_and(|error| error.get_attr(\"text\").is_some() && error.get_attr(\"code\").and_then(|v| __iq_error_code(v.as_str())).is_some_and(|code| {})))", arms.join(" || ") )) } @@ -633,6 +780,7 @@ fn emit_outcome_types( names.push((vname.clone(), format!("{spec_base}{vname}"))); } for (index, (variant, (_, name))) in op.response.variants.iter().zip(&names).enumerate() { + assertion_conditions(&variant.assertions, "response")?; if direct_error_payload_shape(variant).is_some() && direct_error_payload(variant).is_none() { return Err(format!( @@ -671,6 +819,11 @@ fn emit_outcome_types( .iter() .map(|v| { let mut required = fail_required_fields(&v.fields); + // The bounded error selector enforces its required wrapper even + // without a redundant Child assertion in the recovered mirror. + if direct_error_payload(v).is_some() { + required.insert("/child:error".into()); + } for assertion in &v.assertions { if assertion.kind == AssertionKind::Child && let Some(name) = &assertion.name @@ -690,10 +843,11 @@ fn emit_outcome_types( && !assertions_conflict(&op.response.variants[i], &op.response.variants[j]) && !(error_selection(&op.response.variants[i], "response").is_some() && error_selection(&op.response.variants[j], "response").is_some() - && !error_arms_cover( - &op.response.variants[i].error_arms, - &op.response.variants[j].error_arms, - )) + && (!assertions_implied_by(&op.response.variants[i], &op.response.variants[j]) + || !error_outcomes_cover( + &[&op.response.variants[i]], + &op.response.variants[j], + )?)) { return Err(format!( "outcomes.unemittable: response.variants[{i}] {} can shadow response.variants[{j}] {}", @@ -718,9 +872,9 @@ fn emit_outcome_types( && assertions_implied_by(earlier, later) && error_selection(earlier, "response").is_some() }) - .flat_map(|(_, earlier)| earlier.error_arms.iter().cloned()) + .map(|(_, earlier)| earlier) .collect(); - if !preceding.is_empty() && error_arms_cover(&preceding, &later.error_arms) { + if !preceding.is_empty() && error_outcomes_cover(&preceding, later)? { return Err(format!( "outcomes.unemittable: preceding error outcomes cover response.variants[{j}] {}", later.tag @@ -1199,6 +1353,7 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - // existing associated type for source compatibility, but make degradation an // explicit error. An explicit empty outcome still uses its admitted union. let correlation = correlation_requirements(op); + let response_guards = assertion_conditions(&op.response.assertions, "response"); let rejection = if let Err(reason) = &correlation { Some(reason.as_str()) } else if let Some(reason) = outcome_error.as_deref() { @@ -1209,6 +1364,8 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - Some("response.payload_unemittable: recovered fields produced no response payload") } else if !use_union && !can_generate { Some("response.parser_unemittable: parser cannot initialize the recovered payload") + } else if let Err(reason) = &response_guards { + Some(reason.as_str()) } else { None }; @@ -1236,6 +1393,7 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - for (wire, value) in correlation.as_ref().unwrap() { lines.push(format!(" if response.get_attr({}).map(|v| v.as_str()).as_deref() != Some({value}) {{ anyhow::bail!({}); }}", rust_lit(wire), rust_lit(&format!("{}: response.{wire} does not match {value}", op.module_name)))); } + lines.extend(emit_response_guards(op, " ")); if use_union { lines.extend(emit_outcome_parse( op, @@ -1265,6 +1423,7 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - lines.push( " fn parse_response(&self, response: &wacore_binary::NodeRef<'_>) -> Result {".to_string() ); + lines.extend(emit_response_guards(op, " ")); lines.extend(emit_outcome_parse( op, &outcome_info, @@ -1279,6 +1438,7 @@ pub(crate) fn generate_spec(op: &IqStanzaDef, ns_const: &str, spec_name: &str) - // A single-shape FALLBACK (op had variants the outcome union couldn't separate) // mirrors the success shape; guard it so a non-success response fails rather // than decoding to all-defaults. A pure single-shape op adds nothing here. + lines.extend(emit_response_guards(op, " ")); lines.extend(emit_success_guards(op, " ")); lines.extend(emit_response_parser( &op.response.fields, @@ -2665,6 +2825,159 @@ mod tests { } } + fn add_optional_payload_checks(v: &mut ResponseVariant) { + let ir: wa_ir::IqIr = + serde_json::from_str(include_str!("../../../generated/iq/index.json")).unwrap(); + let op = ir + .stanzas + .iter() + .find(|o| o.module_name == "WASmaxOutGroupsSetSubjectRequest") + .unwrap(); + let source = direct_error_payload(&op.response.variants[1]).unwrap(); + let field = source + .union_variants + .as_ref() + .unwrap() + .iter() + .flat_map(|v| &v.fields) + .find(|f| f.method == "child") + .unwrap() + .clone(); + for f in &mut v.fields { + if let Some(variants) = &mut f.union_variants { + for variant in variants { + variant.fields.push(field.clone()); + } + } + } + } + + #[test] + fn payload_failures_keep_pairwise_and_collective_error_fallbacks_reachable() { + for collective in [false, true] { + let mut a = bounded_error_variant(400, if collective { 449 } else { 499 }); + add_optional_payload_checks(&mut a); + let mut b = bounded_error_variant(450, 499); + add_optional_payload_checks(&mut b); + b.assertions.reverse(); + let mut op = stanza("PayloadFallback", None); + op.response.variants = if collective { vec![a, b] } else { vec![a] }; + op.response.variants.push(bounded_error_variant(400, 499)); + let mut emitted = Vec::new(); + assert!( + emit_outcome_types( + &op, + "PayloadFallback", + "PayloadFallbackResponse", + "test", + &mut emitted + ) + .is_ok(), + "binary error content must reach the attribute-only fallback" + ); + } + } + + #[test] + fn implicit_error_envelope_guards_participate_in_coverage() { + let mut earlier = bounded_error_variant(400, 499); + earlier.assertions.push(wa_ir::ResponseAssertion { + kind: AssertionKind::Child, + name: Some("error".into()), + value: None, + reference_path: None, + }); + let mut later = bounded_error_variant(400, 499); + later.assertions.retain(|a| a.kind != AssertionKind::Attr); + assert!(assertions_implied_by(&earlier, &later)); + let mut op = stanza("ImplicitErrorGuards", None); + op.response.variants = vec![earlier, later]; + assert!( + emit_outcome_types(&op, "Implicit", "ImplicitResponse", "test", &mut Vec::new()) + .is_err() + ); + } + + #[test] + fn coverage_checks_payloads_and_diagnoses_unproven_implication() { + let mut earlier = bounded_error_variant(400, 499); + add_optional_payload_checks(&mut earlier); + let later = earlier.clone(); + assert_eq!(error_outcomes_cover(&[&earlier], &later), Ok(true)); + let mut different = later.clone(); + for f in &mut different.fields { + if let Some(variants) = &mut f.union_variants { + for v in variants { + let child = v.fields.iter_mut().find(|f| f.method == "child").unwrap(); + child.children.as_mut().unwrap()[0].parser_required = false; + } + } + } + assert!( + error_outcomes_cover(&[&earlier], &different) + .unwrap_err() + .contains("payload_coverage_unknown") + ); + } + + #[test] + fn unsupported_root_guards_are_diagnosed_and_field_pins_survive_missing_mirrors() { + let mut op = stanza("RootGuard", None); + op.response.fields = bounded_error_variant(400, 499).fields[..1].to_vec(); + for kind in [ + AssertionKind::Tag, + AssertionKind::Attr, + AssertionKind::Content, + AssertionKind::Child, + AssertionKind::FromServer, + ] { + op.response.assertions = vec![wa_ir::ResponseAssertion { + kind, + name: None, + value: None, + reference_path: None, + }]; + let generated = generate_spec(&op, "ROOT_NAMESPACE", "RootGuardSpec"); + assert!(generated.contains("RESPONSE_GENERATION_ERROR")); + assert!(generated.contains("guards.")); + } + let mut v = bounded_error_variant(400, 499); + v.assertions.clear(); + assert!( + error_selection(&v, "response") + .unwrap() + .contains("Some(\"error\")") + ); + let header = v + .fields + .iter_mut() + .find(|f| f.field_type != wa_ir::ParsedFieldType::Union) + .unwrap(); + header.enum_keys = Some(vec!["error".into()]); + assert!(direct_error_payload(&v).is_none()); + } + + #[test] + fn non_union_guards_are_enforced_with_and_without_context() { + let ir: wa_ir::IqIr = + serde_json::from_str(include_str!("../../../generated/iq/index.json")).unwrap(); + let original = ir + .stanzas + .iter() + .find(|o| o.module_name == "WASmaxOutPingsClientRequest") + .unwrap(); + for context in [true, false] { + let mut op = original.clone(); + if !context { + op.response + .assertions + .retain(|a| a.kind != AssertionKind::Reference); + } + let code = generate_spec(&op, "PINGS_NAMESPACE", "PingSpec"); + assert!(code.contains("response.tag() == \"iq\""), "{code}"); + } + } + #[test] fn direct_error_extra_guards_are_rejected_not_omitted() { for assertion in [ @@ -2733,8 +3046,8 @@ mod tests { op.response.variants = vec![variant("iq"), variant("message")]; let code = generate_spec(&op, "TEST_NAMESPACE", "DisjointTagsSpec"); assert!(!code.contains("RESPONSE_GENERATION_ERROR"), "{code}"); - assert!(code.contains("response.tag == \"iq\"")); - assert!(code.contains("response.tag == \"message\"")); + assert!(code.contains("response.tag() == \"iq\"")); + assert!(code.contains("response.tag() == \"message\"")); } #[test] diff --git a/crates/wa-codegen/tests/fixtures/iq_runtime.rs b/crates/wa-codegen/tests/fixtures/iq_runtime.rs index 2fa520b0..ca2ad791 100644 --- a/crates/wa-codegen/tests/fixtures/iq_runtime.rs +++ b/crates/wa-codegen/tests/fixtures/iq_runtime.rs @@ -12,7 +12,6 @@ pub struct Node { } #[derive(Clone, Copy)] pub struct NodeRef<'a> { - pub tag: &'a str, node: &'a Node, } impl Node { @@ -28,13 +27,13 @@ impl Node { } } fn as_ref(&self) -> NodeRef<'_> { - NodeRef { - tag: &self.tag, - node: self, - } + NodeRef { node: self } } } impl<'a> NodeRef<'a> { + pub fn tag(&self) -> &'a str { + &self.node.tag + } pub fn get_attr(&self, name: &str) -> Option<&'a String> { self.node.attrs.get(name) } @@ -190,7 +189,7 @@ fn main() { "invite", 12345, &jid::Jid::new("456", jid::Server::Pn), - target, + target.clone(), ); let request = subject.build_iq(); assert_eq!(request.target.0, "123@g.us"); @@ -205,6 +204,105 @@ fn main() { assert_eq!(nodes[0].attrs["code"], "invite"); assert_eq!(nodes[0].attrs["expiration"], "12345"); assert_eq!(nodes[0].attrs["admin"], "456@s.whatsapp.net"); + let guarded = MakeGuardedPayloadSpec::new(target.clone(), Vec::::new()); + let plain = MakeGuardedPlainSpec::new(target.clone(), Vec::::new()); + let content = MakeGuardedContentSpec::new(target.clone(), Vec::::new()); + let mut good = result(vec![Node::new("proof", &[], vec![])]); + good.attrs.insert("marker".into(), "present".into()); + good.attrs.insert("scope".into(), "group".into()); + assert!( + guarded + .parse_response_with_request(&good.as_ref(), "req-1", "123@g.us") + .is_ok() + ); + assert!(plain.parse_response(&good.as_ref()).is_ok()); + for mutation in 0..7 { + let mut bad = good.clone(); + match mutation { + 0 => bad.tag = "message".into(), + 1 => { + bad.attrs.remove("marker"); + } + 2 => { + bad.attrs.insert("scope".into(), "other".into()); + } + 3 => bad.children.clear(), + 4 => bad.children.push(bad.children[0].clone()), + 5 => { + bad.children.clear(); + bad.bytes = Some(vec![]); + } + _ => { + bad.attrs.insert("type".into(), "error".into()); + } + } + assert!( + guarded + .parse_response_with_request(&bad.as_ref(), "req-1", "123@g.us") + .is_err(), + "context mutation {mutation}" + ); + assert!( + plain.parse_response(&bad.as_ref()).is_err(), + "plain mutation {mutation}" + ); + } + good.children.clear(); + good.bytes = Some(b"accepted".to_vec()); + assert!( + content + .parse_response_with_request(&good.as_ref(), "req-1", "123@g.us") + .is_ok() + ); + good.bytes = Some(b"other".to_vec()); + assert!( + content + .parse_response_with_request(&good.as_ref(), "req-1", "123@g.us") + .is_err() + ); + good.bytes = None; + assert!( + content + .parse_response_with_request(&good.as_ref(), "req-1", "123@g.us") + .is_err() + ); + + let coverage = MakePayloadCoverageSpec::new(target.clone(), Vec::::new()); + for code in ["410", "470"] { + for body in [vec![], b"opaque".to_vec(), vec![0xff]] { + let mut node = error(code, Some("unknown")); + node.children[0].bytes = Some(body); + assert!(matches!( + coverage + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakePayloadCoverageResponse::FallbackError(_) + )); + } + for malformed in [ + vec![Node::new("field", &[("name", "subject")], vec![])], + vec![ + Node::new("field", &[], vec![]), + Node::new("field", &[], vec![]), + ], + ] { + let mut node = error(code, Some("unknown")); + node.children[0].children = malformed; + assert!(matches!( + coverage + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakePayloadCoverageResponse::FallbackError(_) + )); + } + let node = error(code, Some("unknown")); + assert!(!matches!( + coverage + .parse_response_with_request(&node.as_ref(), "req-1", "123@g.us") + .unwrap(), + MakePayloadCoverageResponse::FallbackError(_) + )); + } let bare = result(vec![]); assert!(subject.parse_response(&bare.as_ref()).is_err()); assert!(accept.parse_response(&bare.as_ref()).is_err()); diff --git a/crates/wa-codegen/tests/iq_runtime.rs b/crates/wa-codegen/tests/iq_runtime.rs index 5f8360a5..8ff42af5 100644 --- a/crates/wa-codegen/tests/iq_runtime.rs +++ b/crates/wa-codegen/tests/iq_runtime.rs @@ -13,6 +13,116 @@ fn generated_pilot_requests_and_responses_execute() { ) }); assert_eq!(ir.stanzas.len(), 2); + let subject = ir + .stanzas + .iter() + .find(|o| o.module_name.contains("SetSubject")) + .unwrap() + .clone(); + // Constructed guard probes use the real pilot's fields and request shape; + // they do not claim additional recovered protocol operations. + for (name, context, content) in [ + ("GuardedPayload", true, false), + ("GuardedPlain", false, false), + ("GuardedContent", true, true), + ] { + let mut op = subject.clone(); + op.module_name = name.into(); + op.exported_function = Some(format!("make{name}")); + op.response.fields = subject.response.variants[0].fields.clone(); + op.response.assertions = subject.response.variants[0].assertions.clone(); + if !context { + op.response + .assertions + .retain(|a| a.kind != wa_ir::AssertionKind::Reference); + } + let guard = |kind, name: Option<&str>, value: Option<&str>| wa_ir::ResponseAssertion { + kind, + name: name.map(str::to_string), + value: value.map(str::to_string), + reference_path: None, + }; + op.response + .assertions + .push(guard(wa_ir::AssertionKind::Attr, Some("marker"), None)); + op.response.assertions.push(guard( + wa_ir::AssertionKind::Attr, + Some("scope"), + Some("group"), + )); + op.response.assertions.push(if content { + guard(wa_ir::AssertionKind::Content, None, Some("accepted")) + } else { + guard(wa_ir::AssertionKind::Child, Some("proof"), None) + }); + op.response.variants.clear(); + ir.stanzas.push(op); + } + let mut op = subject.clone(); + op.module_name = "PayloadCoverage".into(); + op.exported_function = Some("makePayloadCoverage".into()); + let original = &subject.response.variants[1]; + let original_payload = original + .fields + .iter() + .find(|f| f.field_type == wa_ir::ParsedFieldType::Union) + .unwrap(); + let optional_field = original_payload + .union_variants + .as_ref() + .unwrap() + .iter() + .flat_map(|v| &v.fields) + .find(|f| f.method == "child") + .unwrap(); + op.response.variants = [ + ("LowError", 400, 449, true), + ("HighError", 450, 499, true), + ("FallbackError", 400, 499, false), + ] + .into_iter() + .map(|(name, lo, hi, child)| { + let mut v = original.clone(); + v.tag = name.into(); + let mut exact = v.error_arms[0].clone(); + exact.code = Some(lo); + let mut range = v.error_arms.last().unwrap().clone(); + range.code_min = Some(lo); + range.code_max = Some(hi); + v.error_arms = vec![exact, range]; + let payload = v + .fields + .iter_mut() + .find(|f| f.field_type == wa_ir::ParsedFieldType::Union) + .unwrap(); + let variants = payload.union_variants.as_ref().unwrap(); + let mut exact = variants[0].clone(); + exact + .fields + .iter_mut() + .find(|f| f.name == "code") + .unwrap() + .literal_value = Some(lo.to_string()); + exact + .assertions + .iter_mut() + .find(|a| a.name.as_deref() == Some("code")) + .unwrap() + .value = Some(lo.to_string()); + let mut range = variants.last().unwrap().clone(); + let code = range.fields.iter_mut().find(|f| f.name == "code").unwrap(); + code.int_min = Some(lo); + code.int_max = Some(hi); + if child { + exact.fields.push(optional_field.clone()); + range.fields.push(optional_field.clone()); + } + payload.union_variants = Some(vec![exact, range]); + v + }) + .collect(); + ir.stanzas.push(op); + let dir = std::env::temp_dir().join(format!("whatspec-iq-runtime-{}", std::process::id())); fs::create_dir_all(&dir).unwrap(); fs::write(dir.join("generated.rs"), wa_codegen::generate_iq(&ir)).unwrap(); diff --git a/docs/iq-response-admission.md b/docs/iq-response-admission.md index c5717da2..e8a7438b 100644 --- a/docs/iq-response-admission.md +++ b/docs/iq-response-admission.md @@ -22,6 +22,8 @@ certified complete. | `response.parser_unemittable` | The generated initializer cannot represent the recovered payload. | | `guards.reference_unsupported` / `guards.reference_nonuniform` | A recovered request reference cannot use the supported uniform wire context. | | `guards.request_context_required` | Call `parse_response_with_request` with the actual request ID and target. | +| `guards.assertion_incomplete` / `guards.from_server_unrecovered` | A guard lacks its required operand or the IR has no recovered server predicate; generation rejects it rather than dropping or inventing it. | +| `outcomes.payload_coverage_unknown` | Optional-child contracts differ and their implication is not established by the bounded model. | | `outcomes.error_contract_unsupported` | A direct error payload has an assertion outside the bounded tag/code/text decoder; no generic fallback may erase that guard. | | `outcomes.unemittable` | A variant parser cannot be emitted, or an earlier variant can shadow a later one under the emitter's known predicates. The message names variant indexes and tags. | @@ -80,6 +82,42 @@ runtime selection. A stricter earlier child guard is a negative control for coverage. These constructed IR cases check generator invariants; they do not claim new server behavior. The real pilot execution tests remain unchanged. +The payload-aware coverage audit also tests the complete path from admission +through compiled execution. Code/text intervals alone are insufficient: an +optional-child decoder can fail on bytes, duplicates or missing child fields. +Both pairwise and collective coverage now count only payload checks implied by +the later arm. Extra earlier children supply concrete counterexamples; differing +contracts on the same child fail admission as `outcomes.payload_coverage_unknown` +until an implication proof is supported. Required error-wrapper and type guards +participate even when their redundant assertion mirrors are absent. Existing +rejection tests remain. + +Response-level tag, attribute presence/literal, content and unique-child guards +use the same predicate emitter as outcome selectors, in both contextual and +context-free entry points. The real Pings parser's tag assertion is checked +against its preserved source; this does not qualify its unresolved JID-enum +semantics. Missing guard operands and the operand-free `from_server` marker are +diagnosed explicitly. + +This exposes seven previously admitted operations with unrecovered server guards: +`UploadPreKeysSpec` (WAWebUploadPreKeysJob), `TitleSpec` +(WAWebQueryCtwaContextJob), `UnpairDeviceSpec` (WAWebUnpairDeviceJob), +`RefreshSpec`, `Refresh2Spec`, `Refresh3Spec` (WAWebTosJob), and +`QueryBusinessProfileJobSpec`. Their associated types remain, but parsing now +returns `guards.from_server_unrecovered`. A reviewed adapter or further source +recovery is required; do not hardcode an assumed server address. This is part of +the unreleased 0.2.0 migration, not rejection of arbitrary extension fields. + +### Reference adapter interface + +The emitted root-tag guard requires `NodeRef::tag() -> &str`. The in-memory +compilation fixture deliberately has no public `tag` field on `NodeRef`, so field +access cannot pass the test. Adapters for this reference output must supply the +accessor when upgrading. No external client crate, event model or dependency is +imported; the existing emitted type paths are reference-adapter names, not proof +of compatibility with a particular downstream crate. Independent conformance +adapters require the same explicit accessor update. + ## Verified pilots | Operation | Request evidence | Ordered outcomes | @@ -148,6 +186,7 @@ credentials were used to extract or test protocol behavior. | `WASmaxOutGroupsAcceptGroupAddRequest` | C | 5461..5920 | | `WASmaxGroupsAcceptGroupAddRPC` | C | 5922..7533 | | `WASmaxParseUtils` | B | 2256336..2263058 | +| `WASmaxInPingsClientResponseServerResponse` | B | 2266135..2266990 | | `WASmaxInGroupsIQErrorResourceConstraintMixin` | D | 623..1153 | These are static observations of this preserved Web build, not proof of the @@ -167,16 +206,16 @@ context. Copying this test to the first fail-closed commit reproduces the missin context API failure. The adapter does not qualify a binary codec or a downstream client; independent conformance remains separately owned. -Two debug generations of all 142 IQ operations produced identical 2,061,750-byte -outputs, SHA-256 `214c9d9fede66c9ff6cf4194e75a506b8fd780b9c56675f0232f864626d6ecbd`, -in 4.43 and 3.92 seconds, with 39 explicitly rejected parsers. The original base +Two debug generations of all 142 IQ operations produced identical 2,067,041-byte +outputs, SHA-256 `b8fb5d0020941e74a1dfc6f2b021844a62982d83e6617d319379c560b40d0742`, +in 4.15 and 4.02 seconds, with 46 explicitly rejected parsers. The original base produced 1,523,859 bytes in 2.84 and 2.98 seconds. The increase includes recovered outcome types and context-taking parsers; it is not an optimization claim. The Rust reference catalog remains ignored by git. Maintenance requires review of the admitted/rejected operation set and the bounded helper against source changes; neither a rejection count nor reduced output size is a quality target. -Reproduce with `cargo test --locked -p wa-codegen`, including 181 unit tests and +Reproduce with `cargo test --locked -p wa-codegen`, including 186 unit tests and the compiled runtime integration test; `cargo clippy --locked -p wa-codegen --all-targets -- -D warnings`; schema validation; and full bundle regeneration with `whatspec update --check`. No generated IR or baseline changes are included. @@ -186,3 +225,14 @@ the separately reviewed quality work from PR #52, including the source-backed baseline reconciliation. This branch integrates that main revision without additional baseline changes. Independent two-snapshot pilot qualification is maintained in PR #55, outside this emitter patch. + +Audit regressions fail at `9de5d0ae8d8561b1036ce3bd875117f2cc152804`: +payload-aware fallback admission and non-union guard checks fail their unit +probes; the accessor-only adapter fails compilation with E0615. After correction, +the compiled fixture additionally exercises both fallback bands with empty, +UTF-8 and non-UTF-8 bytes, malformed/duplicate children, and controls where the +earlier parser succeeds. Contextual and context-free guard probes test wrong +tags, missing/pinned attributes, content, missing/duplicate children and bytes. +These additional contracts are explicitly constructed test cases, not new +claimed source operations. All 579 bundle hashes/sizes and setHash were rechecked +with locked restore before the Pings and helper modules were read by static AST. From b8945bc32609e3b14d1ff332de4d70ecc92aca62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 16:45:27 +0000 Subject: [PATCH 42/52] test: qualify IQ accessor and binary fallback guard updates --- .../tests/fixtures/independent_iq_cases.rs | 4 ++-- tests/conformance/compiled/README.md | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs b/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs index 38590f8f..a6ffc2a0 100644 --- a/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs +++ b/crates/wa-codegen/tests/fixtures/independent_iq_cases.rs @@ -294,7 +294,7 @@ fn binary_error_content_reaches_fallback() { #[test] fn absent_and_empty_binary_error_content_remain_distinct() { let subject = specs().0; - for binary in [None, Some(vec![])] { + for binary in [None, Some(vec![]), Some(b"opaque".to_vec()), Some(vec![0xff])] { let mut response = error("406", Some("not-acceptable")); response.children[0].bytes = binary.clone(); let MakeSetSubjectRequestResponse::ClientError(value) = subject @@ -311,7 +311,7 @@ fn absent_and_empty_binary_error_content_remain_distinct() { assert_eq!(fallback, binary.is_some(), "{value:?}"); } // Parsers that inspect only attributes must still accept binary content. - for bytes in [vec![], b"opaque".to_vec()] { + for bytes in [vec![], b"opaque".to_vec(), vec![0xff]] { let mut response = error("499", Some("unknown")); response.children[0].bytes = Some(bytes); assert_eq!(subject_class(&response).unwrap(), "client"); diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index c91640ef..478ec999 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -357,3 +357,20 @@ and fails the pinned test, covering both snapshots and all record groups. Unmodified evidence passes all six composition fixture tests. Updating a capture now requires reviewing its complete provenance binding and deliberately updating the corresponding pin, in addition to source/selected-input integrity checks. + +### Payload guards and reference adapter update + +Composition includes IQ owner commit `4471cbc3bc1b07e71ef15627281ac149cd41e91d`. +The independent harness reuses only its adapter/constructors before `fn main`, +so it now compiles with `NodeRef::tag()` and no public tag field. Its eight +source-backed groups and expected outcomes remain independently maintained. +The absent/empty binary regression also covers UTF-8 and non-UTF-8 bytes: the +child-reading specific error falls through, while attribute-only errors accept +those bytes. No external client dependency or compatibility certification is added. + +Owner regressions additionally exercise constructed payload-coverage and +response-level guard contracts. They are generator invariants, not recovered +server operations. Seven newly diagnosed `from_server` contracts bring the +owner catalog's explicit rejection count to 46; see `docs/iq-response-admission.md` +for the operation list and generation size/time. The two independently captured +pilot inputs, source evidence and provenance pins are unchanged. From 3cb0863d8336a52abf800f3442aa6752ce1b68d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 17:05:02 +0000 Subject: [PATCH 43/52] test: close independent oracle coverage and fixture inventory gaps --- .github/workflows/ci.yml | 6 +- .../wa-mex/tests/independent_conformance.rs | 82 +++++++--- .../wa-scan/tests/independent_conformance.rs | 154 +++++++++++++----- tests/conformance/README.md | 36 +++- tests/conformance/test_capture.py | 64 +++++++- 5 files changed, 262 insertions(+), 80 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f69a14c..79817d11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.x' # Uses only the standard library and committed fixtures. No bundle restore diff --git a/crates/wa-mex/tests/independent_conformance.rs b/crates/wa-mex/tests/independent_conformance.rs index 26f2a2c6..7de76bda 100644 --- a/crates/wa-mex/tests/independent_conformance.rs +++ b/crates/wa-mex/tests/independent_conformance.rs @@ -19,31 +19,65 @@ fn verified_call_sites_keep_presence_separate_from_boolean_type() { + &source(version, "WAWebMexFetchNewsletterJob"); let ir = wa_mex::extract_mex(&source, version); let op = &ir.operations["FetchNewsletter"]; - for (key, expected) in [ - ("fetch_creation_time", VariablePresence::Always), - ("fetch_full_image", VariablePresence::Always), - ("fetch_viewer_metadata", VariablePresence::Conditional), - ("fetch_pinned_messages", VariablePresence::Undetermined), - ] { - assert_eq!( - op.variables_presence[key].presence, expected, - "{version}: {key}" - ); - // The scalar-name heuristic types these fetch_* names as boolean. - // That does not establish call-site nullability or key presence. - assert_eq!(op.variables_shape[key], TypeNode::Leaf("boolean".into())); + reviewed_presence(op); + } +} + +fn reviewed_presence(op: &wa_ir::MexOperation) { + for (key, expected) in [ + ("fetch_creation_time", VariablePresence::Always), + ("fetch_full_image", VariablePresence::Always), + ("fetch_status_metadata", VariablePresence::Always), + ("fetch_wamo_sub", VariablePresence::Always), + ("fetch_viewer_metadata", VariablePresence::Conditional), + ("fetch_pinned_messages", VariablePresence::Undetermined), + ] { + assert_eq!(op.variables_presence[key].presence, expected, "{key}"); + // The scalar-name heuristic types these fetch_* names as boolean. + // That does not establish call-site nullability or key presence. + assert_eq!(op.variables_shape[key], TypeNode::Leaf("boolean".into())); + } + assert_eq!( + op.variables_presence["input"].presence, + VariablePresence::Always + ); + assert_eq!( + op.variables_presence["input"].fields["key"].presence, + VariablePresence::Conditional + ); + assert_eq!( + op.variables_presence["input"].fields["view_role"].presence, + VariablePresence::Conditional + ); + assert_eq!( + op.variables_presence["input"].fields["type"].presence, + VariablePresence::Always + ); +} + +#[test] +fn presence_oracle_rejects_regressions_in_all_reviewed_keys() { + let source = source( + "2.3000.1047483476", + "WAWebMexFetchNewsletterJobQuery.graphql", + ) + &source("2.3000.1047483476", "WAWebMexFetchNewsletterJob"); + let ir = wa_mex::extract_mex(&source, "fixture"); + for key in ["fetch_status_metadata", "fetch_wamo_sub", "view_role"] { + let mut op = ir.operations["FetchNewsletter"].clone(); + if key == "view_role" { + op.variables_presence + .get_mut("input") + .unwrap() + .fields + .get_mut(key) + .unwrap() + .presence = VariablePresence::Always; + } else { + op.variables_presence.get_mut(key).unwrap().presence = VariablePresence::Undetermined; } - assert_eq!( - op.variables_presence["input"].presence, - VariablePresence::Always - ); - assert_eq!( - op.variables_presence["input"].fields["key"].presence, - VariablePresence::Conditional - ); - assert_eq!( - op.variables_presence["input"].fields["type"].presence, - VariablePresence::Always + assert!( + std::panic::catch_unwind(|| reviewed_presence(&op)).is_err(), + "accepted {key} regression" ); } } diff --git a/crates/wa-scan/tests/independent_conformance.rs b/crates/wa-scan/tests/independent_conformance.rs index 0239c32a..0a449893 100644 --- a/crates/wa-scan/tests/independent_conformance.rs +++ b/crates/wa-scan/tests/independent_conformance.rs @@ -47,29 +47,23 @@ fn request_contract(ir: &Value) { assert_eq!(request["target"], "group_jid"); assert_eq!(request["targetArgPath"], json!([{"key":"iqTo"}])); } - let subject = &operation(ir, "SetSubject")["request"]["children"][0]; - assert_eq!(subject["tag"], "subject"); assert_eq!( - subject["content"]["argPath"], - json!([{"key":"subjectElementValue"}]) + operation(ir, "SetSubject")["request"]["children"], + json!([{ + "tag":"subject", "attrs":[], "children":[], "repeats":false, + "content":{"kind":"dynamic","argPath":[{"key":"subjectElementValue"}]} + }]) + ); + assert_eq!( + operation(ir, "AcceptGroupAdd")["request"]["children"], + json!([{ + "tag":"accept", "attrs":[ + {"name":"code","kind":"string","required":true,"argPath":[{"key":"acceptCode"}]}, + {"name":"expiration","kind":"integer","required":true,"argPath":[{"key":"acceptExpiration"}]}, + {"name":"admin","kind":"user_jid","required":true,"argPath":[{"key":"acceptAdmin"}]} + ], "children":[], "repeats":false + }]) ); - let accept = &operation(ir, "AcceptGroupAdd")["request"]["children"][0]; - assert_eq!(accept["tag"], "accept"); - for (wire, kind, arg) in [ - ("code", "string", "acceptCode"), - ("expiration", "integer", "acceptExpiration"), - ("admin", "user_jid", "acceptAdmin"), - ] { - let attr = accept["attrs"] - .as_array() - .unwrap() - .iter() - .find(|a| a["name"] == wire) - .unwrap(); - assert_eq!(attr["kind"], kind); - assert_eq!(attr["required"], true); - assert_eq!(attr["argPath"], json!([{"key":arg}])); - } } // Deliberately restricted IR interpreter for success assertions. No field coercion, @@ -109,7 +103,19 @@ fn success<'a>(op: &'a Value, response: &Value, request: &Value) -> Option<&'a s .map(|v| v["tag"].as_str().unwrap()) } -fn response_cases(ir: &Value) { +fn response_cases(ir: &Value, complete_error_vocabulary: bool) { + // The 13-module success capture omits error parsers, so only the full + // snapshot can refine code families. Never infer the side from tag names. + let client = if complete_error_vocabulary { + "client_error" + } else { + "error" + }; + let server = if complete_error_vocabulary { + "server_error" + } else { + "error" + }; let request = json!({"id":"fixture-7","to":"120363000000001@g.us"}); let bare = json!({"tag":"iq","attrs":{"id":"fixture-7","from":"120363000000001@g.us","type":"result"},"children":[]}); let mut child = bare.clone(); @@ -187,21 +193,33 @@ fn response_cases(ir: &Value) { } } } - let tags: Vec<_> = accept["response"]["variants"] - .as_array() - .unwrap() - .iter() - .map(|v| v["tag"].as_str().unwrap()) - .collect(); - assert_eq!( - tags, - [ - "AcceptGroupAddResponseGroupJoinRequestSuccess", - "AcceptGroupAddResponseSuccess", - "AcceptGroupAddResponseClientError", - "AcceptGroupAddResponseServerError" - ] - ); + for (op, expected) in [ + ( + accept, + vec![ + ("AcceptGroupAddResponseGroupJoinRequestSuccess", "success"), + ("AcceptGroupAddResponseSuccess", "success"), + ("AcceptGroupAddResponseClientError", "error"), + ("AcceptGroupAddResponseServerError", server), + ], + ), + ( + subject, + vec![ + ("SetSubjectResponseSuccess", "success"), + ("SetSubjectResponseClientError", client), + ("SetSubjectResponseServerError", server), + ], + ), + ] { + let outcomes: Vec<_> = op["response"]["variants"] + .as_array() + .unwrap() + .iter() + .map(|v| (v["tag"].as_str().unwrap(), v["kind"].as_str().unwrap())) + .collect(); + assert_eq!(outcomes, expected); + } } #[test] @@ -209,7 +227,7 @@ fn source_derived_contracts_hold_across_two_verified_snapshots() { for version in VERSIONS { let ir = captured(version); request_contract(&ir); - response_cases(&ir); + response_cases(&ir, false); } } @@ -218,7 +236,7 @@ fn committed_ir_satisfies_the_independent_wire_cases() { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../generated/iq/index.json"); let ir = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); request_contract(&ir); - response_cases(&ir); + response_cases(&ir, true); } #[test] @@ -247,3 +265,61 @@ fn the_oracle_detects_lost_child_gate_and_reordered_outcomes() { Some("AcceptGroupAddResponseGroupJoinRequestSuccess") ); } + +#[test] +fn request_oracle_rejects_extra_wire_structure() { + let ir = captured(VERSIONS[1]); + for name in ["SetSubject", "AcceptGroupAdd"] { + for mutation in ["sibling", "attribute", "nested", "repeated"] { + let mut changed = ir.clone(); + let op = changed["stanzas"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|s| s["moduleName"] == format!("WASmaxOutGroups{name}Request")) + .unwrap(); + let children = op["request"]["children"].as_array_mut().unwrap(); + match mutation { + "sibling" => children.push(json!({"tag":"unreviewed"})), + "attribute" => children[0]["attrs"] + .as_array_mut() + .unwrap() + .push(json!({"name":"unreviewed","kind":"string"})), + "nested" => children[0]["children"] + .as_array_mut() + .unwrap() + .push(json!({"tag":"unreviewed"})), + _ => children[0]["repeats"] = json!(true), + } + assert!( + std::panic::catch_unwind(|| request_contract(&changed)).is_err(), + "accepted {name} {mutation}" + ); + } + } +} + +#[test] +fn response_oracle_rejects_misclassified_error_outcomes() { + let ir = captured(VERSIONS[1]); + for name in ["SetSubject", "AcceptGroupAdd"] { + let count = operation(&ir, name)["response"]["variants"] + .as_array() + .unwrap() + .len(); + for index in count - 2..count { + let mut changed = ir.clone(); + let op = changed["stanzas"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|s| s["moduleName"] == format!("WASmaxOutGroups{name}Request")) + .unwrap(); + op["response"]["variants"][index]["kind"] = json!("unclassified"); + assert!( + std::panic::catch_unwind(|| response_cases(&changed, false)).is_err(), + "accepted {name} outcome {index}" + ); + } + } +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md index 531d6c4f..b9119b01 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -184,13 +184,13 @@ time target/release/whatspec update --bundles BUNDLES --wa-version VERSION --out python3 scripts/validate-schemas.py OUTPUT_1 ``` -Current lint remains red on six *improved* exact baselines: unresolved IQ -attribute paths 49 vs 55, child paths 10 vs 12, MEX operations without established -presence 10 vs 13, undetermined MEX variables 106 vs 108, WAM constructions with -no catalog entry 29 vs 41, and unread WAM arguments 93 vs 104. Baselines and CI -were not changed here, apart from adding the independent fixture-test job. -Published-SHA CI and combined emitter qualification are -still required before declaring the repository qualified. +At the original baseline, six improved exact counters differed from the then +committed baseline. The reviewed #52 reconciliation is now on main and exact +lint passes without relaxed limits. CI for baseline head +`9356dee13ee2e4b36592287ebeb5a06cb0dce72c` passed all three jobs on 2026-10-08 +(run 37801197673). Later review found oracle coverage gaps; follow-up changes +require their own published-head checks. Combined emitter qualification remains +separate in #55 and is not implied by the static baseline. ## Post-quality integration @@ -207,3 +207,25 @@ Changes to setHash, bundle counts/bytes, per-module bundle identity or offsets must fail the offline check until deliberately reviewed. These pins were checked against the recorded historical bundle locks; recapture does not update them automatically. Hash pins detect drift, not authenticity of coordinated edits. + +## Oracle completeness follow-up + +The request oracle compares the complete reviewed child arrays, including leaf +shape, attributes, content and repetition. Outcome order includes kind for both +pilots. The 13-module capture contains success parsers but omits error +vocabularies, so its error kinds remain `error`. The full artifact refines +SetSubject to `client_error`/`server_error` and AcceptGroupAdd to +`error`/`server_error`. The mixed 304/4xx/500 AcceptGroupAdd client arm cannot +be assigned a single code family. These expectations are explicit, not inferred +from variant names or copied dynamically from extracted IR. All captured MEX presence cases are checked, +including status metadata, wamo subscription and nested view_role. + +Fixture file names must equal the reviewed inventory, preventing unprovenanced +JavaScript from entering the scan. Capture checks module completeness after all +verified bundles are scanned: a module missing from one bundle is normal; a +module missing from the entire set fails before creating output. The orchestration +regression stubs span output and does not claim to retest AST semantics. + +The new CI job pins checkout/setup-python to the commits returned for their v5 +tags on 2026-10-08 and disables checkout credential persistence. No other job's +action policy or concurrency is changed. diff --git a/tests/conformance/test_capture.py b/tests/conformance/test_capture.py index 75ebcbf5..0a63e20d 100644 --- a/tests/conformance/test_capture.py +++ b/tests/conformance/test_capture.py @@ -1,6 +1,7 @@ """Offline checks for capture identity and corrupted input rejection.""" import hashlib import json +import shutil from pathlib import Path import tempfile import unittest @@ -69,14 +70,61 @@ def test_reviewed_provenance_bindings_are_pinned(self): def test_committed_sources_match_every_recorded_span_hash(self): for version in ['2.3000.1045368834', '2.3000.1047483476']: root = Path(__file__).parent / version - evidence = json.loads((root / 'provenance.json').read_text()) - self.assertEqual({e['module'] for e in evidence['modules']}, set(MODULES)) - for entry in evidence['modules']: - source = (root / (entry['module'] + '.js')).read_bytes() - self.assertTrue(source.endswith(b';\n')) - source = source[:-2] # Capture's terminator, not part of the AST span. - self.assertEqual(len(source), entry['end'] - entry['start']) - self.assertEqual(hashlib.sha256(source).hexdigest(), entry['sourceSha256']) + self.assert_source_set(root) + + def assert_source_set(self, root): + self.assertEqual({p.name for p in root.glob("*.js")}, + {name + ".js" for name in MODULES}) + evidence = json.loads((root / 'provenance.json').read_text()) + self.assertEqual({e['module'] for e in evidence['modules']}, set(MODULES)) + for entry in evidence['modules']: + source = (root / (entry['module'] + '.js')).read_bytes() + self.assertTrue(source.endswith(b';\n')) + source = source[:-2] # Capture's terminator, not part of the AST span. + self.assertEqual(len(source), entry['end'] - entry['start']) + self.assertEqual(hashlib.sha256(source).hexdigest(), entry['sourceSha256']) + + def test_unprovenanced_js_is_rejected(self): + for version in ['2.3000.1045368834', '2.3000.1047483476']: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / 'snapshot' + shutil.copytree(Path(__file__).parent / version, root) + (root / 'WASmaxUnreviewed.js').write_text('__d("Unreviewed",[],function(){});') + with self.assertRaises(AssertionError): + self.assert_source_set(root) + + def test_missing_modules_are_checked_across_the_complete_bundle_set(self): + # Stub only the span subprocess: verify capture orchestration, not AST semantics. + for missing in [False, True]: + with self.subTest(missing=missing), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + bundles = root / 'bundles' + bundles.mkdir() + first = b'__d("Other",[],function(){});' + second = b'__d("Wanted",[],function(){});' + records = [] + for name, raw in [('a.js', first), ('b.js', second)]: + (bundles / name).write_bytes(raw) + records.append(dict(sha256=hashlib.sha256(raw).hexdigest(), size=len(raw))) + fingerprint = hashlib.sha256('\n'.join(sorted( + f"{r['sha256']}:{r['size']}" for r in records)).encode()).hexdigest() + lock = root / 'lock.json' + lock.write_text(json.dumps(dict(waVersion='fixture', bundleCount=2, + setHash=fingerprint, bundles=records))) + def spans(args, **kwargs): + raw = kwargs['input'] + return b'' if missing or raw == first else f'Wanted\t0\t{len(raw)-1}\n'.encode() + with patch('capture.MODULES', ['Wanted']), patch( + 'capture.subprocess.check_output', side_effect=spans + ) as parser: + if missing: + with self.assertRaisesRegex(ValueError, 'missing modules'): + capture(lock, bundles, root / 'output') + self.assertFalse((root / 'output').exists()) + else: + capture(lock, bundles, root / 'output') + self.assertTrue((root / 'output/Wanted.js').exists()) + self.assertEqual(parser.call_count, 2) def test_changed_missing_extra_and_wrong_set_hash_are_rejected_before_ast(self): for failure in ['changed', 'missing', 'extra', 'setHash', 'count']: From 8581d292d54dd8b6224f744ced96226733453c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 17:16:06 +0000 Subject: [PATCH 44/52] test: assert complete presence keys and mirrored IQ contracts --- .../wa-mex/tests/independent_conformance.rs | 48 +++++++++++++++++++ .../wa-scan/tests/independent_conformance.rs | 39 ++++++++++++++- tests/conformance/README.md | 7 +++ 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/crates/wa-mex/tests/independent_conformance.rs b/crates/wa-mex/tests/independent_conformance.rs index 7de76bda..ae08d45f 100644 --- a/crates/wa-mex/tests/independent_conformance.rs +++ b/crates/wa-mex/tests/independent_conformance.rs @@ -24,6 +24,29 @@ fn verified_call_sites_keep_presence_separate_from_boolean_type() { } fn reviewed_presence(op: &wa_ir::MexOperation) { + let keys: std::collections::BTreeSet<_> = + op.variables_presence.keys().map(String::as_str).collect(); + assert_eq!( + keys, + std::collections::BTreeSet::from([ + "input", + "fetch_creation_time", + "fetch_full_image", + "fetch_status_metadata", + "fetch_wamo_sub", + "fetch_viewer_metadata", + "fetch_pinned_messages", + ]) + ); + let input_keys: std::collections::BTreeSet<_> = op.variables_presence["input"] + .fields + .keys() + .map(String::as_str) + .collect(); + assert_eq!( + input_keys, + std::collections::BTreeSet::from(["key", "type", "view_role"]) + ); for (key, expected) in [ ("fetch_creation_time", VariablePresence::Always), ("fetch_full_image", VariablePresence::Always), @@ -115,3 +138,28 @@ fn null_undefined_and_unresolved_calls_have_distinct_presence_verdicts() { ); } } + +#[test] +fn presence_oracle_rejects_unreviewed_keys() { + let source = source( + "2.3000.1047483476", + "WAWebMexFetchNewsletterJobQuery.graphql", + ) + &source("2.3000.1047483476", "WAWebMexFetchNewsletterJob"); + let ir = wa_mex::extract_mex(&source, "fixture"); + for nested in [false, true] { + let mut op = ir.operations["FetchNewsletter"].clone(); + if nested { + let input = op.variables_presence.get_mut("input").unwrap(); + input + .fields + .insert("unreviewed".into(), input.fields["key"].clone()); + } else { + op.variables_presence + .insert("unreviewed".into(), op.variables_presence["input"].clone()); + } + assert!( + std::panic::catch_unwind(|| reviewed_presence(&op)).is_err(), + "accepted extra key, nested={nested}" + ); + } +} diff --git a/crates/wa-scan/tests/independent_conformance.rs b/crates/wa-scan/tests/independent_conformance.rs index 0a449893..03ee0b9d 100644 --- a/crates/wa-scan/tests/independent_conformance.rs +++ b/crates/wa-scan/tests/independent_conformance.rs @@ -41,7 +41,11 @@ fn operation<'a>(ir: &'a Value, name: &str) -> &'a Value { fn request_contract(ir: &Value) { for name in ["SetSubject", "AcceptGroupAdd"] { - let request = &operation(ir, name)["request"]; + let op = operation(ir, name); + assert_eq!(op["namespace"], "w:g2"); + assert_eq!(op["iqType"], "set"); + assert_eq!(op["target"], "group_jid"); + let request = &op["request"]; assert_eq!(request["namespace"], "w:g2"); assert_eq!(request["iqType"], "set"); assert_eq!(request["target"], "group_jid"); @@ -125,6 +129,13 @@ fn response_cases(ir: &Value, complete_error_vocabulary: bool) { // This interpreter only covers the reviewed empty-payload successes. A new // field needs an explicit extension, not accidental acceptance by this model. for op in [accept, subject] { + assert_eq!( + op["response"]["fields"], + json!([{ + "method":"attrString", "name":"type", "wireName":"type", + "type":"string", "parserRequired":true, "literalValue":"result" + }]) + ); for variant in op["response"]["variants"] .as_array() .unwrap() @@ -323,3 +334,29 @@ fn response_oracle_rejects_misclassified_error_outcomes() { } } } + +#[test] +fn oracle_rejects_outer_metadata_and_missing_response_mirror() { + let ir = captured(VERSIONS[1]); + for name in ["SetSubject", "AcceptGroupAdd"] { + for field in ["namespace", "iqType", "target", "response_fields"] { + let mut changed = ir.clone(); + let op = changed["stanzas"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|s| s["moduleName"] == format!("WASmaxOutGroups{name}Request")) + .unwrap(); + if field == "response_fields" { + op["response"]["fields"] = json!([]); + assert!(std::panic::catch_unwind(|| response_cases(&changed, false)).is_err()); + } else { + op[field] = json!("wrong"); + assert!( + std::panic::catch_unwind(|| request_contract(&changed)).is_err(), + "accepted {name} outer {field}" + ); + } + } + } +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md index b9119b01..1fafeeb3 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -229,3 +229,10 @@ regression stubs span output and does not claim to retest AST semantics. The new CI job pins checkout/setup-python to the commits returned for their v5 tags on 2026-10-08 and disables checkout credential persistence. No other job's action policy or concurrency is changed. + +The oracle also rejects extra top-level/nested MEX presence keys and checks the +outer stanza namespace/type/target consumed by generation, independently of the +request copy. Both pilots' top-level response fields must contain the reviewed +result-type field, matching the primary success payload without deriving the +expected value from the tested variant. Mutation cases cover these mirrored +contracts; they establish extraction consistency, not additional server rules. From d663231a74fa5fa00a319874e3892c2f59bcaadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 18:12:37 +0000 Subject: [PATCH 45/52] test: bind IQ fixtures to Cargo artifacts and selected rustc --- crates/wa-codegen/build.rs | 14 ++ crates/wa-codegen/tests/fixture_compiler.rs | 142 ++++++++++++++++++++ crates/wa-codegen/tests/iq_runtime.rs | 30 +---- crates/wa-codegen/tests/support/mod.rs | 105 +++++++++++++++ docs/iq-response-admission.md | 28 ++++ 5 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 crates/wa-codegen/build.rs create mode 100644 crates/wa-codegen/tests/fixture_compiler.rs create mode 100644 crates/wa-codegen/tests/support/mod.rs diff --git a/crates/wa-codegen/build.rs b/crates/wa-codegen/build.rs new file mode 100644 index 00000000..c2800613 --- /dev/null +++ b/crates/wa-codegen/build.rs @@ -0,0 +1,14 @@ +fn main() { + // Cargo can select rustc through configuration even when the test process + // has no RUSTC override. Keep that exact default for runtime fixtures. + for (source, destination) in [ + ("RUSTC", "WHATSPEC_FIXTURE_RUSTC"), + ("TARGET", "WHATSPEC_FIXTURE_TARGET"), + ] { + println!("cargo:rerun-if-env-changed={source}"); + println!( + "cargo:rustc-env={destination}={}", + std::env::var(source).expect("Cargo build environment") + ); + } +} diff --git a/crates/wa-codegen/tests/fixture_compiler.rs b/crates/wa-codegen/tests/fixture_compiler.rs new file mode 100644 index 00000000..a388d00f --- /dev/null +++ b/crates/wa-codegen/tests/fixture_compiler.rs @@ -0,0 +1,142 @@ +//! Dependency selection regressions independent of the IQ emitter. +mod support; +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; + +fn directory(label: &str) -> PathBuf { + let root = std::env::temp_dir().join(format!( + "whatspec-fixture-compiler-{label}-{}", + std::process::id() + )); + fs::create_dir(&root).expect("new regression directory"); + root +} + +fn run_fixture(compiler: &support::FixtureCompiler, root: &Path) { + let compiled = compiler.command(root, false).output().unwrap(); + assert!( + compiled.status.success(), + "{}", + String::from_utf8_lossy(&compiled.stderr) + ); + assert!(Command::new(root.join("run")).status().unwrap().success()); +} + +#[test] +fn cargo_artifact_binding_ignores_invalid_and_other_build_rlibs() { + let root = directory("artifacts"); + let deps = root + .join("dependency-target") + .join(env!("WHATSPEC_FIXTURE_TARGET")) + .join("debug/deps"); + fs::create_dir_all(&deps).unwrap(); + let stale = deps.join("libanyhow-other-build.rlib"); + let invalid = deps.join("libanyhow-invalid.rlib"); + fs::write(root.join("stale.rs"), "pub fn stale_artifact_marker() {}\n").unwrap(); + let rustc = support::selected_rustc(std::env::var_os("RUSTC")); + let compiled = Command::new(&rustc) + .args([ + "--crate-name", + "anyhow", + "--crate-type", + "rlib", + "--target", + env!("WHATSPEC_FIXTURE_TARGET"), + ]) + .arg(root.join("stale.rs")) + .arg("-o") + .arg(&stale) + .output() + .unwrap(); + assert!( + compiled.status.success(), + "{}", + String::from_utf8_lossy(&compiled.stderr) + ); + fs::write(&invalid, b"not Rust metadata").unwrap(); + fs::write( + root.join("main.rs"), + "fn main() -> anyhow::Result<()> { Ok(()) }\n", + ) + .unwrap(); + // Negative controls establish that these files cannot satisfy this fixture. + for wrong in [&stale, &invalid] { + let rejected = Command::new(&rustc) + .args([ + "--edition=2024", + "--target", + env!("WHATSPEC_FIXTURE_TARGET"), + ]) + .arg(root.join("main.rs")) + .arg("--extern") + .arg(format!("anyhow={}", wrong.display())) + .arg("-o") + .arg(root.join("wrong-run")) + .output() + .unwrap(); + assert!( + !rejected.status.success(), + "wrong artifact unexpectedly compiled: {}", + wrong.display() + ); + } + let compiler = support::FixtureCompiler::build(&root); + let command = compiler.command(&root, false); + let selected = command + .get_args() + .find(|arg| arg.to_string_lossy().starts_with("anyhow=")) + .unwrap(); + assert_ne!(selected, format!("anyhow={}", stale.display()).as_str()); + assert_ne!(selected, format!("anyhow={}", invalid.display()).as_str()); + run_fixture(&compiler, &root); + // Both contaminated candidates still exist: success did not depend on cleaning + // the directory. Also exercise Cargo's fresh-artifact report on the second build. + assert!(stale.exists() && invalid.exists()); + run_fixture(&support::FixtureCompiler::build(&root), &root); + fs::remove_dir_all(root).unwrap(); +} + +#[cfg(unix)] +#[test] +fn rustc_override_compiles_both_dependency_and_fixture() { + use std::os::unix::fs::PermissionsExt; + let root = directory("override"); + let rustc = support::selected_rustc(std::env::var_os("RUSTC")); + let wrapper = root.join("rustc-wrapper"); + let log = root.join("rustc.log"); + let quote = |value: &str| format!("'{}'", value.replace('\'', "'\\''")); + fs::write( + &wrapper, + format!( + "#!/bin/sh\nprintf '%s\\n' \"$*\" >> {}\nexec {} \"$@\"\n", + quote(log.to_str().unwrap()), + quote(rustc.to_str().unwrap()) + ), + ) + .unwrap(); + fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o700)).unwrap(); + fs::write( + root.join("main.rs"), + "fn main() -> anyhow::Result<()> { Ok(()) }\n", + ) + .unwrap(); + let selected = support::selected_rustc(Some(wrapper.clone().into_os_string())); + assert_eq!(selected, wrapper.as_os_str()); + let compiler = support::FixtureCompiler::build_with_rustc(&root, selected); + run_fixture(&compiler, &root); + let calls = fs::read_to_string(log).unwrap(); + assert!( + calls + .lines() + .any(|line| line.contains("--crate-name anyhow")), + "{calls}" + ); + assert!( + calls.lines().any(|line| line.contains("main.rs")), + "{calls}" + ); + fs::remove_dir_all(root).unwrap(); +} diff --git a/crates/wa-codegen/tests/iq_runtime.rs b/crates/wa-codegen/tests/iq_runtime.rs index 8ff42af5..2661de0b 100644 --- a/crates/wa-codegen/tests/iq_runtime.rs +++ b/crates/wa-codegen/tests/iq_runtime.rs @@ -1,6 +1,7 @@ //! Compile and execute the emitted pilot parsers against a small in-memory node //! adapter. This checks generated Rust, not a binary codec or external client API. use std::{fs, process::Command}; +mod support; #[test] fn generated_pilot_requests_and_responses_execute() { @@ -127,33 +128,8 @@ fn generated_pilot_requests_and_responses_execute() { fs::create_dir_all(&dir).unwrap(); fs::write(dir.join("generated.rs"), wa_codegen::generate_iq(&ir)).unwrap(); fs::write(dir.join("main.rs"), include_str!("fixtures/iq_runtime.rs")).unwrap(); - let deps = std::env::current_exe() - .unwrap() - .parent() - .unwrap() - .to_path_buf(); - let anyhow = fs::read_dir(&deps) - .unwrap() - .map(|e| e.unwrap().path()) - .find(|p| { - p.file_name() - .unwrap() - .to_string_lossy() - .starts_with("libanyhow-") - && p.extension().is_some_and(|ext| ext == "rlib") - }) - .expect("anyhow dev dependency rlib"); - let compile = Command::new("rustc") - .arg("--edition=2024") - .arg(dir.join("main.rs")) - .arg("--extern") - .arg(format!("anyhow={}", anyhow.display())) - .arg("-L") - .arg(format!("dependency={}", deps.display())) - .arg("-o") - .arg(dir.join("run")) - .output() - .unwrap(); + let compiler = support::FixtureCompiler::build(&dir); + let compile = compiler.command(&dir, false).output().unwrap(); assert!( compile.status.success(), "generated pilot compilation failed:\n{}", diff --git a/crates/wa-codegen/tests/support/mod.rs b/crates/wa-codegen/tests/support/mod.rs new file mode 100644 index 00000000..5e030eba --- /dev/null +++ b/crates/wa-codegen/tests/support/mod.rs @@ -0,0 +1,105 @@ +//! Compile fixtures using a Cargo-reported artifact, never a directory heuristic. +use std::{ + ffi::OsString, + fs, + path::{Path, PathBuf}, + process::Command, +}; + +pub struct FixtureCompiler { + rustc: OsString, + anyhow: PathBuf, +} + +impl FixtureCompiler { + pub fn build(directory: &Path) -> Self { + Self::build_with_rustc(directory, selected_rustc(std::env::var_os("RUSTC"))) + } + + pub fn build_with_rustc(directory: &Path, rustc: OsString) -> Self { + let target = directory.join("dependency-target"); + let output = Command::new(env!("CARGO")) + .args([ + "build", + "--offline", + "--locked", + "-p", + "anyhow", + "--message-format=json", + ]) + .arg("--manifest-path") + .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")) + .arg("--target-dir") + .arg(&target) + .arg("--target") + .arg(env!("WHATSPEC_FIXTURE_TARGET")) + .env("RUSTC", &rustc) + .output() + .expect("run Cargo for fixture dependency"); + assert!( + output.status.success(), + "fixture dependency build failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let mut artifacts = Vec::new(); + for line in output + .stdout + .split(|b| *b == b'\n') + .filter(|line| !line.is_empty()) + { + let message: serde_json::Value = + serde_json::from_slice(line).expect("Cargo JSON message"); + if message["reason"] == "compiler-artifact" && message["target"]["name"] == "anyhow" { + for file in message["filenames"].as_array().expect("artifact filenames") { + let path = PathBuf::from(file.as_str().expect("artifact path")); + if path.extension().is_some_and(|ext| ext == "rlib") { + artifacts.push(path); + } + } + } + } + assert_eq!( + artifacts.len(), + 1, + "expected exactly one Cargo-reported anyhow rlib: {artifacts:?}" + ); + let anyhow = artifacts + .pop() + .unwrap() + .canonicalize() + .expect("reported rlib exists"); + assert!( + anyhow.starts_with(target.canonicalize().unwrap()), + "artifact must belong to the isolated build" + ); + // Preserve the binding beside generated.rs for diagnosing a failed fixture. + fs::write(directory.join("dependency-artifact.json"), &output.stdout).unwrap(); + Self { rustc, anyhow } + } + + pub fn command(&self, directory: &Path, test: bool) -> Command { + let mut command = Command::new(&self.rustc); + command + .arg("--edition=2024") + .arg("--target") + .arg(env!("WHATSPEC_FIXTURE_TARGET")) + .arg(directory.join("main.rs")) + .arg("--extern") + .arg(format!("anyhow={}", self.anyhow.display())) + .arg("-L") + .arg(format!( + "dependency={}", + self.anyhow.parent().unwrap().display() + )) + .arg("-o") + .arg(directory.join("run")); + if test { + command.arg("--test"); + } + command + } +} + +pub fn selected_rustc(override_path: Option) -> OsString { + override_path.unwrap_or_else(|| env!("WHATSPEC_FIXTURE_RUSTC").into()) +} diff --git a/docs/iq-response-admission.md b/docs/iq-response-admission.md index e8a7438b..179189e5 100644 --- a/docs/iq-response-admission.md +++ b/docs/iq-response-admission.md @@ -236,3 +236,31 @@ tags, missing/pinned attributes, content, missing/duplicate children and bytes. These additional contracts are explicitly constructed test cases, not new claimed source operations. All 579 bundle hashes/sizes and setHash were rechecked with locked restore before the Pings and helper modules were read by static AST. + +### Fixture dependency/compiler binding + +The compiled runtime fixture builds only the lock-selected `anyhow` package via +Cargo `--offline --locked` in an isolated target directory. It consumes exactly +one `compiler-artifact` rlib path from that invocation's JSON output. It does +not scan `deps`, select by mtime, or guess an artifact hash. Multiple/absent +reported rlibs fail explicitly. Isolation avoids nesting a build under the +parent test build's target-directory lock. + +A dependency-free build script records Cargo's selected `RUSTC` and target. +The fixture compiler uses the explicit runtime `RUSTC` override when present, +otherwise that recorded compiler, for both dependency build and fixture. +There is no new production dependency and no IQ emitter/IR change. + +Regression controls leave invalid metadata and a same-toolchain rlib from a +different crate build alongside the Cargo artifact. Compiling against each +wrong artifact fails; the reported artifact compiles and executes, including +a repeated build using Cargo's fresh-artifact report. A Unix compiler wrapper +records both dependency and fixture invocations to check override propagation. +Only one installed toolchain was used; no cross-version E0514 reproduction is +claimed. The tests qualify the reference fixture build, not an external client. + +Observed local wall times after the outer test build: runtime integration +3.97 seconds and the two infrastructure regressions together 3.67 seconds. +These are single shared-executor observations, not performance claims. Each +fixture temporarily builds `anyhow`; successful fixture directories are removed, +while failed fixtures retain generated source and Cargo artifact messages. From 92f2adf7d36bddfdc00104cb239bd7362a3a7475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 18:14:21 +0000 Subject: [PATCH 46/52] test: use Cargo-bound compiler for independent IQ snapshots --- crates/wa-codegen/tests/independent_iq.rs | 31 +++-------------------- tests/conformance/compiled/README.md | 15 +++++++++++ 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/crates/wa-codegen/tests/independent_iq.rs b/crates/wa-codegen/tests/independent_iq.rs index 3099f976..8e64b5ad 100644 --- a/crates/wa-codegen/tests/independent_iq.rs +++ b/crates/wa-codegen/tests/independent_iq.rs @@ -1,6 +1,7 @@ //! Independent cases read from verified Web module bodies, executed against //! freshly emitted Rust. Requires the explicit #51 + #53 composition. use std::{fs, process::Command}; +mod support; fn qualify(label: &str, input: &str) { let mut ir: wa_ir::IqIr = serde_json::from_str(input).unwrap(); @@ -33,34 +34,8 @@ fn qualify(label: &str, input: &str) { ), ) .unwrap(); - let deps = std::env::current_exe() - .unwrap() - .parent() - .unwrap() - .to_path_buf(); - let anyhow = fs::read_dir(&deps) - .unwrap() - .map(|e| e.unwrap().path()) - .find(|p| { - p.file_name() - .unwrap() - .to_string_lossy() - .starts_with("libanyhow-") - && p.extension().is_some_and(|e| e == "rlib") - }) - .unwrap(); - let compile = Command::new("rustc") - .arg("--edition=2024") - .arg("--test") - .arg(dir.join("main.rs")) - .arg("--extern") - .arg(format!("anyhow={}", anyhow.display())) - .arg("-L") - .arg(format!("dependency={}", deps.display())) - .arg("-o") - .arg(dir.join("run")) - .output() - .unwrap(); + let compiler = support::FixtureCompiler::build(&dir); + let compile = compiler.command(&dir, true).output().unwrap(); assert!( compile.status.success(), "{}", diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index 478ec999..ddcd6af3 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -374,3 +374,18 @@ server operations. Seven newly diagnosed `from_server` contracts bring the owner catalog's explicit rejection count to 46; see `docs/iq-response-admission.md` for the operation list and generation size/time. The two independently captured pilot inputs, source evidence and provenance pins are unchanged. + +### Cargo-bound fixture compilation + +Both runtime harnesses now use the shared test compiler from #53. It builds the +lock-selected `anyhow` with Cargo offline in an isolated target directory and +uses that invocation's exact JSON artifact path. No deps-directory order or +mtime inference remains. The fixture and dependency use the same explicit +`RUSTC` override, or Cargo's compiler recorded at package build time. + +The independent expectations and adapter boundary are unchanged. Infrastructure +regressions leave invalid and other-build rlibs in place, reject those explicit +negative controls, and execute with the Cargo-reported artifact. A wrapper +checks compiler override propagation. This is compilation infrastructure, not +additional protocol evidence or a downstream compatibility claim. See the IQ +admission document for costs and the single-toolchain test limitation. From a7b0cde6cc2a89fa175eb241f32fb02e031ced05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 19:36:02 +0000 Subject: [PATCH 47/52] test: pin pilot exports, captured catalog and archive origins --- .../wa-scan/tests/independent_conformance.rs | 70 ++++++++++++++++++- tests/conformance/README.md | 33 +++++---- tests/conformance/origins.json | 16 +++++ tests/conformance/test_capture.py | 33 ++++++++- 4 files changed, 133 insertions(+), 19 deletions(-) create mode 100644 tests/conformance/origins.json diff --git a/crates/wa-scan/tests/independent_conformance.rs b/crates/wa-scan/tests/independent_conformance.rs index 03ee0b9d..729d9be0 100644 --- a/crates/wa-scan/tests/independent_conformance.rs +++ b/crates/wa-scan/tests/independent_conformance.rs @@ -42,6 +42,7 @@ fn operation<'a>(ir: &'a Value, name: &str) -> &'a Value { fn request_contract(ir: &Value) { for name in ["SetSubject", "AcceptGroupAdd"] { let op = operation(ir, name); + assert_eq!(op["exportedFunction"], format!("make{name}Request")); assert_eq!(op["namespace"], "w:g2"); assert_eq!(op["iqType"], "set"); assert_eq!(op["target"], "group_jid"); @@ -233,12 +234,32 @@ fn response_cases(ir: &Value, complete_error_vocabulary: bool) { } } +fn captured_contract(ir: &Value) { + // Only the small source capture has this exact catalog; the full artifact + // legitimately contains additional operations. + let mut modules: Vec<_> = ir["stanzas"] + .as_array() + .unwrap() + .iter() + .map(|s| s["moduleName"].as_str().unwrap()) + .collect(); + modules.sort_unstable(); + assert_eq!( + modules, + [ + "WASmaxOutGroupsAcceptGroupAddRequest", + "WASmaxOutGroupsSetSubjectRequest" + ] + ); + request_contract(ir); + response_cases(ir, false); +} + #[test] fn source_derived_contracts_hold_across_two_verified_snapshots() { for version in VERSIONS { let ir = captured(version); - request_contract(&ir); - response_cases(&ir, false); + captured_contract(&ir); } } @@ -360,3 +381,48 @@ fn oracle_rejects_outer_metadata_and_missing_response_mirror() { } } } + +#[test] +fn oracle_rejects_changed_or_missing_exported_functions() { + let ir = captured(VERSIONS[1]); + for name in ["SetSubject", "AcceptGroupAdd"] { + for value in [Some(json!("wrong")), Some(Value::Null), None] { + let mut changed = ir.clone(); + let op = changed["stanzas"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|s| s["moduleName"] == format!("WASmaxOutGroups{name}Request")) + .unwrap(); + match value { + Some(value) => { + op["exportedFunction"] = value; + } + None => { + op.as_object_mut().unwrap().remove("exportedFunction"); + } + } + assert!( + std::panic::catch_unwind(|| request_contract(&changed)).is_err(), + "accepted changed/missing {name} export" + ); + } + } +} + +#[test] +fn captured_oracle_rejects_extra_or_duplicate_stanzas() { + for version in VERSIONS { + let ir = captured(version); + for name in ["Unreviewed", "SetSubject", "AcceptGroupAdd"] { + let mut changed = ir.clone(); + let mut extra = operation(&ir, "SetSubject").clone(); + extra["moduleName"] = json!(format!("WASmaxOutGroups{name}Request")); + changed["stanzas"].as_array_mut().unwrap().push(extra); + assert!( + std::panic::catch_unwind(|| captured_contract(&changed)).is_err(), + "accepted extra {name} in {version}" + ); + } + } +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md index 1fafeeb3..c21113fe 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -12,25 +12,22 @@ static verification, not execution of the JavaScript builders or parsers. ## Evidence -Captured from the exact `bundle-store` assets for these commits. Both manifests -use schema version 4.3.0. No account, credentials or session data were used. +Captured from exact `bundle-store` assets. Both manifests use schema version +4.3.0. No account, credentials or session data were used. -| Snapshot | Repository commit | Bundles | Uncompressed JS bytes | -| --- | --- | ---: | ---: | -| 2.3000.1045368834 | `1a441f0329c941fcdb238490a6c604550d8a9939` | 516 | 77,347,149 | -| 2.3000.1047483476 | `1f5167c4cebf263edaf001e06a2497f213c55793` | 579 | 75,824,128 | +[origins.json](origins.json) is the authoritative, pinned mapping from each +snapshot to its repository commit, measured archive SHA-256, setHash and capture +provenance hash. These values record the original verified downloads; adding this +mapping does not claim a fresh download or recapture. The offline test binds each +origin to the corresponding capture and rejects identifier drift. -The old setHash is -`99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00`. -The current setHash is -`05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b`. +| Snapshot | Bundles | Uncompressed JS bytes | +| --- | ---: | ---: | +| 2.3000.1045368834 | 516 | 77,347,149 | +| 2.3000.1047483476 | 579 | 75,824,128 | The asset name is `bundles--.tar.xz` under [the bundle-store release](https://github.com/oxidezap/whatspec/releases/tag/bundle-store). -The measured archive SHA-256 digests are, respectively: - -- `2edd4b3f8dae50b503e0b15920adc774e2680cdb542396d57608bb0374196ee1` -- `22243b9c2ff18a66cea8fd66551821fe0f9f09786d107e2be8284637db86c349` Each snapshot's `provenance.json` records all selected definitions, bundle SHA-256, byte start/end with exclusive end, and source hash. The fixture bytes are the @@ -236,3 +233,11 @@ request copy. Both pilots' top-level response fields must contain the reviewed result-type field, matching the primary success payload without deriving the expected value from the tested variant. Mutation cases cover these mirrored contracts; they establish extraction consistency, not additional server rules. + +The oracle also pins the two exported request function names, which determine +public spec names, and the exact module-name multiset of the small source +capture. That catalog constraint is not applied to the full generated artifact. +Mutations of exports (changed/null/missing) and extra/duplicate stanzas reproduce +the former gaps and are rejected. The origin-record pins additionally cover the +historical repository commits and archive hashes previously documented only in +this README. Pins detect drift, not coordinated edits to evidence and test pins. diff --git a/tests/conformance/origins.json b/tests/conformance/origins.json new file mode 100644 index 00000000..79675d3f --- /dev/null +++ b/tests/conformance/origins.json @@ -0,0 +1,16 @@ +[ + { + "waVersion": "2.3000.1045368834", + "repositoryCommit": "1a441f0329c941fcdb238490a6c604550d8a9939", + "archiveSha256": "2edd4b3f8dae50b503e0b15920adc774e2680cdb542396d57608bb0374196ee1", + "setHash": "99a75bd7a4961e15051172c8b99fc460d57e58f7eb47ea9c46824657dd083e00", + "captureProvenanceSha256": "41e4e797af7ab73dc7b3cd1e63e033b264c4bd6aa0bc6f7157262064ed78c13a" + }, + { + "waVersion": "2.3000.1047483476", + "repositoryCommit": "1f5167c4cebf263edaf001e06a2497f213c55793", + "archiveSha256": "22243b9c2ff18a66cea8fd66551821fe0f9f09786d107e2be8284637db86c349", + "setHash": "05609307e68b0f6ccfd2a121a573049999e38b752cfe7155ebbe5c661805751b", + "captureProvenanceSha256": "30f9ee277d45596ca423bbb2e24b513ee2a7ca220d01bce0978255cd19d7ffe8" + } +] diff --git a/tests/conformance/test_capture.py b/tests/conformance/test_capture.py index 0a63e20d..86b0afac 100644 --- a/tests/conformance/test_capture.py +++ b/tests/conformance/test_capture.py @@ -53,10 +53,12 @@ def test_single_quoted_and_escaped_names_reach_the_ast(self): def test_reviewed_provenance_bindings_are_pinned(self): # These complete-record pins are reviewed anchors, not regenerated by - # capture.py. They include bundle identities/counts/offsets and, for - # selected inputs, the source commit/path/full artifact hash. Updating - # a capture requires reviewing these bindings before changing its pin. + # capture.py. Snapshot pins cover bundle identities/counts/offsets. + # origins.json binds each capture to its historical commit and archive. + # A new capture requires reviewing the evidence before changing pins. pins = { + 'origins.json': + '2621e336dfabf40ba9b52c3f4b55c4da8dba1e7a82759f2d978be5f6fa18e559', '2.3000.1045368834/provenance.json': '41e4e797af7ab73dc7b3cd1e63e033b264c4bd6aa0bc6f7157262064ed78c13a', '2.3000.1047483476/provenance.json': @@ -67,6 +69,31 @@ def test_reviewed_provenance_bindings_are_pinned(self): raw = (Path(__file__).parent / relative).read_bytes() self.assertEqual(hashlib.sha256(raw).hexdigest(), expected) + def assert_origin_pin(self, raw): + self.assertEqual(hashlib.sha256(raw).hexdigest(), '2621e336dfabf40ba9b52c3f4b55c4da8dba1e7a82759f2d978be5f6fa18e559') + + def test_origin_records_bind_the_capture_and_reject_identifier_drift(self): + root = Path(__file__).parent + raw = (root / 'origins.json').read_bytes() + self.assert_origin_pin(raw) + records = json.loads(raw) + self.assertEqual([r['waVersion'] for r in records], + ['2.3000.1045368834', '2.3000.1047483476']) + for index, record in enumerate(records): + provenance = (root / record['waVersion'] / 'provenance.json').read_bytes() + self.assertEqual(hashlib.sha256(provenance).hexdigest(), + record['captureProvenanceSha256']) + self.assertEqual(json.loads(provenance)['setHash'], record['setHash']) + for field in ['repositoryCommit', 'archiveSha256']: + changed = json.loads(raw) + changed[index][field] = '0' * len(record[field]) + with self.subTest(snapshot=record['waVersion'], field=field): + with self.assertRaises(AssertionError): + self.assert_origin_pin((json.dumps(changed, indent=2) + '\n').encode()) + swapped = list(reversed(records)) + with self.assertRaises(AssertionError): + self.assert_origin_pin((json.dumps(swapped, indent=2) + '\n').encode()) + def test_committed_sources_match_every_recorded_span_hash(self): for version in ['2.3000.1045368834', '2.3000.1047483476']: root = Path(__file__).parent / version From b36fd1745c8d629275825704a327af653e67b678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 19:44:43 +0000 Subject: [PATCH 48/52] test: assert complete captured MEX catalog and variable shape --- .../wa-mex/tests/independent_conformance.rs | 101 +++++++++++++++++- tests/conformance/README.md | 7 ++ 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/crates/wa-mex/tests/independent_conformance.rs b/crates/wa-mex/tests/independent_conformance.rs index ae08d45f..60159598 100644 --- a/crates/wa-mex/tests/independent_conformance.rs +++ b/crates/wa-mex/tests/independent_conformance.rs @@ -1,5 +1,5 @@ //! Presence is a serialization property, separate from inferred type/nullability. -use std::path::Path; +use std::{collections::BTreeMap, path::Path}; use wa_ir::{TypeNode, VariablePresence}; fn source(version: &str, module: &str) -> String { @@ -18,12 +18,42 @@ fn verified_call_sites_keep_presence_separate_from_boolean_type() { let source = source(version, "WAWebMexFetchNewsletterJobQuery.graphql") + &source(version, "WAWebMexFetchNewsletterJob"); let ir = wa_mex::extract_mex(&source, version); - let op = &ir.operations["FetchNewsletter"]; - reviewed_presence(op); + reviewed_catalog(&ir); } } +fn reviewed_catalog(ir: &wa_ir::MexIr) { + assert_eq!( + ir.operations.keys().map(String::as_str).collect::>(), + ["FetchNewsletter"] + ); + reviewed_presence(&ir.operations["FetchNewsletter"]); +} + fn reviewed_presence(op: &wa_ir::MexOperation) { + // The source fixes object structure and argument names. Scalar tags remain + // the documented name-based approximation, not GraphQL/server type proof. + let boolean = || TypeNode::Leaf("boolean".into()); + let string = || TypeNode::Leaf("string".into()); + assert_eq!( + op.variables_shape, + BTreeMap::from([ + ("fetch_creation_time".into(), boolean()), + ("fetch_full_image".into(), boolean()), + ("fetch_status_metadata".into(), boolean()), + ("fetch_wamo_sub".into(), boolean()), + ("fetch_viewer_metadata".into(), boolean()), + ("fetch_pinned_messages".into(), boolean()), + ( + "input".into(), + TypeNode::Object(BTreeMap::from([ + ("key".into(), string()), + ("type".into(), string()), + ("view_role".into(), string()), + ])) + ), + ]) + ); let keys: std::collections::BTreeSet<_> = op.variables_presence.keys().map(String::as_str).collect(); assert_eq!( @@ -163,3 +193,68 @@ fn presence_oracle_rejects_unreviewed_keys() { ); } } + +#[test] +fn source_catalog_rejects_spurious_mex_operations() { + for version in ["2.3000.1045368834", "2.3000.1047483476"] { + let source = source(version, "WAWebMexFetchNewsletterJobQuery.graphql") + + &source(version, "WAWebMexFetchNewsletterJob"); + let mut ir = wa_mex::extract_mex(&source, version); + ir.operations.insert( + "Unreviewed".into(), + ir.operations["FetchNewsletter"].clone(), + ); + assert!(std::panic::catch_unwind(|| reviewed_catalog(&ir)).is_err()); + } +} + +#[test] +fn variables_shape_oracle_rejects_missing_extra_and_changed_fields() { + let source = source( + "2.3000.1047483476", + "WAWebMexFetchNewsletterJobQuery.graphql", + ) + &source("2.3000.1047483476", "WAWebMexFetchNewsletterJob"); + let ir = wa_mex::extract_mex(&source, "fixture"); + let original = &ir.operations["FetchNewsletter"]; + for nested in [false, true] { + let keys: Vec<_> = if nested { + vec!["key", "type", "view_role"] + } else { + vec![ + "input", + "fetch_creation_time", + "fetch_full_image", + "fetch_status_metadata", + "fetch_wamo_sub", + "fetch_viewer_metadata", + "fetch_pinned_messages", + ] + }; + for key in keys.into_iter().chain(["unreviewed"]) { + for remove in [false, true] { + if key == "unreviewed" && remove { + continue; + } + let mut op = original.clone(); + let fields = if nested { + let TypeNode::Object(fields) = op.variables_shape.get_mut("input").unwrap() + else { + panic!("input must be an object") + }; + fields + } else { + &mut op.variables_shape + }; + if remove { + fields.remove(key); + } else { + fields.insert(key.into(), TypeNode::Leaf("wrong".into())); + } + assert!( + std::panic::catch_unwind(|| reviewed_presence(&op)).is_err(), + "accepted shape mutation {key}, nested={nested}, remove={remove}" + ); + } + } + } +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md index c21113fe..82dc2292 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -241,3 +241,10 @@ Mutations of exports (changed/null/missing) and extra/duplicate stanzas reproduc the former gaps and are rejected. The origin-record pins additionally cover the historical repository commits and archive hashes previously documented only in this README. Pins detect drift, not coordinated edits to evidence and test pins. + +The MEX oracle compares the complete captured operation catalog (`FetchNewsletter`) +and the full variables shape: six boolean flags plus an `input` object with +`key`, `type` and `view_role` string leaves. Structure and names come from the +preserved caller/Relay argument definitions; scalar tags are the documented +name-based approximation, not proven server types or nullability. Mutations +cover extra operations and changed/missing/extra top-level and nested fields. From 700310fd7c470d80f90506a68ad10d7ae380e570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 19:56:30 +0000 Subject: [PATCH 49/52] test: compare complete MEX presence nodes --- .../wa-mex/tests/independent_conformance.rs | 133 +++++++++++------- tests/conformance/README.md | 5 + 2 files changed, 88 insertions(+), 50 deletions(-) diff --git a/crates/wa-mex/tests/independent_conformance.rs b/crates/wa-mex/tests/independent_conformance.rs index 60159598..bd89f9d6 100644 --- a/crates/wa-mex/tests/independent_conformance.rs +++ b/crates/wa-mex/tests/independent_conformance.rs @@ -1,6 +1,6 @@ //! Presence is a serialization property, separate from inferred type/nullability. use std::{collections::BTreeMap, path::Path}; -use wa_ir::{TypeNode, VariablePresence}; +use wa_ir::{TypeNode, VariablePresence, VariablePresenceNode}; fn source(version: &str, module: &str) -> String { std::fs::read_to_string( @@ -54,58 +54,43 @@ fn reviewed_presence(op: &wa_ir::MexOperation) { ), ]) ); - let keys: std::collections::BTreeSet<_> = - op.variables_presence.keys().map(String::as_str).collect(); + let leaf = |presence| VariablePresenceNode { + presence, + fields: BTreeMap::new(), + items: None, + }; assert_eq!( - keys, - std::collections::BTreeSet::from([ - "input", - "fetch_creation_time", - "fetch_full_image", - "fetch_status_metadata", - "fetch_wamo_sub", - "fetch_viewer_metadata", - "fetch_pinned_messages", + op.variables_presence, + BTreeMap::from([ + ("fetch_creation_time".into(), leaf(VariablePresence::Always)), + ("fetch_full_image".into(), leaf(VariablePresence::Always)), + ( + "fetch_status_metadata".into(), + leaf(VariablePresence::Always) + ), + ("fetch_wamo_sub".into(), leaf(VariablePresence::Always)), + ( + "fetch_viewer_metadata".into(), + leaf(VariablePresence::Conditional) + ), + ( + "fetch_pinned_messages".into(), + leaf(VariablePresence::Undetermined) + ), + ( + "input".into(), + VariablePresenceNode { + presence: VariablePresence::Always, + fields: BTreeMap::from([ + ("key".into(), leaf(VariablePresence::Conditional)), + ("type".into(), leaf(VariablePresence::Always)), + ("view_role".into(), leaf(VariablePresence::Conditional)), + ]), + items: None, + } + ), ]) ); - let input_keys: std::collections::BTreeSet<_> = op.variables_presence["input"] - .fields - .keys() - .map(String::as_str) - .collect(); - assert_eq!( - input_keys, - std::collections::BTreeSet::from(["key", "type", "view_role"]) - ); - for (key, expected) in [ - ("fetch_creation_time", VariablePresence::Always), - ("fetch_full_image", VariablePresence::Always), - ("fetch_status_metadata", VariablePresence::Always), - ("fetch_wamo_sub", VariablePresence::Always), - ("fetch_viewer_metadata", VariablePresence::Conditional), - ("fetch_pinned_messages", VariablePresence::Undetermined), - ] { - assert_eq!(op.variables_presence[key].presence, expected, "{key}"); - // The scalar-name heuristic types these fetch_* names as boolean. - // That does not establish call-site nullability or key presence. - assert_eq!(op.variables_shape[key], TypeNode::Leaf("boolean".into())); - } - assert_eq!( - op.variables_presence["input"].presence, - VariablePresence::Always - ); - assert_eq!( - op.variables_presence["input"].fields["key"].presence, - VariablePresence::Conditional - ); - assert_eq!( - op.variables_presence["input"].fields["view_role"].presence, - VariablePresence::Conditional - ); - assert_eq!( - op.variables_presence["input"].fields["type"].presence, - VariablePresence::Always - ); } #[test] @@ -258,3 +243,51 @@ fn variables_shape_oracle_rejects_missing_extra_and_changed_fields() { } } } + +#[test] +fn presence_oracle_rejects_children_and_items_on_scalar_leaves() { + let source = source( + "2.3000.1047483476", + "WAWebMexFetchNewsletterJobQuery.graphql", + ) + &source("2.3000.1047483476", "WAWebMexFetchNewsletterJob"); + let ir = wa_mex::extract_mex(&source, "fixture"); + for path in [ + "fetch_creation_time", + "fetch_full_image", + "fetch_status_metadata", + "fetch_wamo_sub", + "fetch_viewer_metadata", + "fetch_pinned_messages", + "key", + "type", + "view_role", + "input", + ] { + for items in [false, true] { + if path == "input" && !items { + continue; + } + let mut op = ir.operations["FetchNewsletter"].clone(); + let extra = op.variables_presence["fetch_full_image"].clone(); + let node = if ["key", "type", "view_role"].contains(&path) { + op.variables_presence + .get_mut("input") + .unwrap() + .fields + .get_mut(path) + .unwrap() + } else { + op.variables_presence.get_mut(path).unwrap() + }; + if items { + node.items = Some(Box::new(extra)); + } else { + node.fields.insert("unreviewed".into(), extra); + } + assert!( + std::panic::catch_unwind(|| reviewed_presence(&op)).is_err(), + "accepted presence structure {path}, items={items}" + ); + } + } +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md index 82dc2292..d6f93a51 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -248,3 +248,8 @@ and the full variables shape: six boolean flags plus an `input` object with preserved caller/Relay argument definitions; scalar tags are the documented name-based approximation, not proven server types or nullability. Mutations cover extra operations and changed/missing/extra top-level and nested fields. + +Presence is compared as a complete tree, including empty scalar `fields` and +absent `items` for all nodes in this object-only capture. Nineteen structural +mutations (children/items on each scalar and items on input) fail the preceding +oracle's regression test and are rejected by the full-tree comparison. From 83c824f6468eaf3bc0b04b83d72ad4c0bc9449d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 19:56:31 +0000 Subject: [PATCH 50/52] docs: distinguish current pilot qualification from historical failures --- tests/conformance/compiled/README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/conformance/compiled/README.md b/tests/conformance/compiled/README.md index ddcd6af3..f7f3b47d 100644 --- a/tests/conformance/compiled/README.md +++ b/tests/conformance/compiled/README.md @@ -1,9 +1,20 @@ # Compiled qualification of #51 and #53 -Status: **blocked by a reproduced source/output divergence**. This test is meant -for an explicitly stacked qualification change after the IQ workstream addresses -the finding. Do not silently add it to #51 alone: it requires #53's generated -context-taking API and small runtime adapter. +Status: **both snapshot suites pass all eight independent compiled groups**. +The reproduced binary-content divergence was fixed in the IQ workstream, and the +original expectations still pass. The current composition uses main +`cf3291ddf62d06175d57577d2c973520a25116a8`, which includes the user-merged #53 +emitter and shared fixture compiler. #51 supplies the independent extraction +baseline. Current published-head CI/review status is tracked in PR #55; local +suite success alone does not mark the PR ready. + +The sections below retain the chronological investigation, including the initial +failure, then its fix and later qualification. Intermediate pending findings, +commit IDs, test counts and timings are historical evidence, not current status. +This remains bounded generated-Rust/static-source qualification, not a live-server +or external-client certification. + +## Initial composition (historical) The local `test/composed-iq-qualification` branch composes these immutable heads: @@ -15,7 +26,7 @@ No published head was changed. No emitter or other IQ-workstream file was edited The local merge preserved `wa-codegen` 0.2.0 and the exact audit dependency versions from #54. -## Method and inputs +## Initial method and inputs (historical) `crates/wa-codegen/tests/independent_iq.rs` runs `generate_iq` twice for each snapshot and checks byte identity. It compiles each resulting Rust source with @@ -44,7 +55,7 @@ the default target directory, then run: python3 tests/conformance/compiled/capture_errors.py LOCK BUNDLES NEW_OUTPUT ``` -## Executed results +## Initial executed results (historical failure) For **each** WA snapshot, 2.3000.1045368834 and 2.3000.1047483476: @@ -67,7 +78,7 @@ The difference is the snapshot version in the generated header. The two-snapshot compile-and-run test phase completed in about 0.82 seconds after building its Rust test driver. This is a single shared-executor observation, not a benchmark. -## Minimal finding for the IQ workstream +## Original finding for the IQ workstream (fixed) Use a correlated SetSubject response with this tree: @@ -119,7 +130,7 @@ On failure the generated source and compiled executable are retained in the reported temporary directory. Run that executable with `--exact binary_error_content_reaches_fallback` for the minimal failing case. -## Limits and next step +## Initial limits and requested next step (historical) This executes generated Rust, not the Web client or server. The adapter is a small tree implementation satisfying the reference emitter's existing paths; From 33c2b3d616587b49ac865b1d7326024d0ccc9d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 20:07:37 +0000 Subject: [PATCH 51/52] test: bind MEX input triage to reviewed operation identity --- .../wa-mex/tests/independent_conformance.rs | 32 +++++++++++++++++-- tests/conformance/README.md | 12 +++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/crates/wa-mex/tests/independent_conformance.rs b/crates/wa-mex/tests/independent_conformance.rs index bd89f9d6..043006ba 100644 --- a/crates/wa-mex/tests/independent_conformance.rs +++ b/crates/wa-mex/tests/independent_conformance.rs @@ -1,6 +1,7 @@ +//! Bounded input-variable triage, not response-selection or complete MEX coverage. //! Presence is a serialization property, separate from inferred type/nullability. use std::{collections::BTreeMap, path::Path}; -use wa_ir::{TypeNode, VariablePresence, VariablePresenceNode}; +use wa_ir::{MexOperationKind, TypeNode, VariablePresence, VariablePresenceNode}; fn source(version: &str, module: &str) -> String { std::fs::read_to_string( @@ -27,7 +28,12 @@ fn reviewed_catalog(ir: &wa_ir::MexIr) { ir.operations.keys().map(String::as_str).collect::>(), ["FetchNewsletter"] ); - reviewed_presence(&ir.operations["FetchNewsletter"]); + let op = &ir.operations["FetchNewsletter"]; + // Reviewed from the preserved Relay params literal, not generated output. + assert_eq!(op.original_name, "WAWebMexFetchNewsletterJobQuery"); + assert_eq!(op.operation_kind, MexOperationKind::Query); + assert_eq!(op.doc_id, "27456920720571478"); + reviewed_presence(op); } fn reviewed_presence(op: &wa_ir::MexOperation) { @@ -291,3 +297,25 @@ fn presence_oracle_rejects_children_and_items_on_scalar_leaves() { } } } + +#[test] +fn source_catalog_rejects_changed_operation_identity() { + let source = source( + "2.3000.1047483476", + "WAWebMexFetchNewsletterJobQuery.graphql", + ) + &source("2.3000.1047483476", "WAWebMexFetchNewsletterJob"); + let original = wa_mex::extract_mex(&source, "fixture"); + for field in ["name", "kind", "doc_id"] { + let mut ir = original.clone(); + let op = ir.operations.get_mut("FetchNewsletter").unwrap(); + match field { + "name" => op.original_name = "Unreviewed".into(), + "kind" => op.operation_kind = MexOperationKind::Mutation, + _ => op.doc_id = "0".into(), + } + assert!( + std::panic::catch_unwind(|| reviewed_catalog(&ir)).is_err(), + "accepted changed operation {field}" + ); + } +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md index d6f93a51..bd4dc2a1 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -1,8 +1,8 @@ # Independent conformance baseline This baseline checks extraction against reviewed Web source for SetSubject, -AcceptGroupAdd and FetchNewsletter. It adds no consumer dependency or runtime -API. It does not qualify the reference Rust emitter or the server protocol. +AcceptGroupAdd, plus bounded FetchNewsletter input-variable triage. It adds no +consumer dependency or runtime API. It does not qualify the reference Rust emitter or the server protocol. The IQ test extracts the captured modules afresh and compares the resulting contract with hand-reviewed expectations. It also checks the committed IQ IR. @@ -65,6 +65,14 @@ qualification remains pending integration with the other workstreams. ## MEX triage +Scope: operation identity and input-variable presence, null handling and inferred +type. The Relay name, query kind and persisted ID bind the reviewed caller to +this exact operation. Response-selection extraction, MEX response structs and +complete MEX generation are not qualified by this baseline. A mutation confined +to `op.response` is therefore outside this oracle's promised coverage; reviewing +that complete tree would be a separate response-conformance expansion. No +production response-extraction defect was reproduced in this workstream. + The complete captured FetchNewsletter job and GraphQL module are identical in both snapshots. `fetch_full_image` and coerced flags are always present; `fetch_viewer_metadata` is conditional because a bare property read can be From e9a1fb0a56e8d4370cc625b167dd08332be516c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Thu, 8 Oct 2026 22:54:53 +0000 Subject: [PATCH 52/52] test: pin outer IQ guards, parser identities and fragment diagnostics --- .../wa-scan/tests/independent_conformance.rs | 100 ++++++++++++++++++ tests/conformance/README.md | 9 ++ 2 files changed, 109 insertions(+) diff --git a/crates/wa-scan/tests/independent_conformance.rs b/crates/wa-scan/tests/independent_conformance.rs index 729d9be0..f1647c0f 100644 --- a/crates/wa-scan/tests/independent_conformance.rs +++ b/crates/wa-scan/tests/independent_conformance.rs @@ -43,6 +43,11 @@ fn request_contract(ir: &Value) { for name in ["SetSubject", "AcceptGroupAdd"] { let op = operation(ir, name); assert_eq!(op["exportedFunction"], format!("make{name}Request")); + assert_eq!(op["parserName"], format!("WASmaxGroups{name}RPC")); + assert_eq!( + op["response"]["parserName"], + format!("WASmaxGroups{name}RPC") + ); assert_eq!(op["namespace"], "w:g2"); assert_eq!(op["iqType"], "set"); assert_eq!(op["target"], "group_jid"); @@ -130,6 +135,9 @@ fn response_cases(ir: &Value, complete_error_vocabulary: bool) { // This interpreter only covers the reviewed empty-payload successes. A new // field needs an explicit extension, not accidental acceptance by this model. for op in [accept, subject] { + // The RPC wrappers delegate envelope guards to each ordered outcome. + // They add no outer guard that would constrain every alternative. + assert_eq!(op["response"]["assertions"], json!([])); assert_eq!( op["response"]["fields"], json!([{ @@ -235,6 +243,30 @@ fn response_cases(ir: &Value, complete_error_vocabulary: bool) { } fn captured_contract(ir: &Value) { + // These three preserved sources export merge*Mixin combinators. They are + // folded into the two requests, not additional standalone IQ operations. + // The reason is the published extractor diagnostic, not a server claim. + let mut exclusions: Vec<_> = ir["unparseable"] + .as_array() + .unwrap() + .iter() + .map(|entry| { + ( + entry["moduleName"].as_str().unwrap(), + entry["reason"].as_str().unwrap(), + ) + }) + .collect(); + exclusions.sort_unstable(); + let reason = "mixin fragment (folded into requests, not a standalone stanza)"; + assert_eq!( + exclusions, + [ + ("WASmaxOutGroupsBaseIQSetRequestMixin", reason), + ("WASmaxOutGroupsBaseSetGroupMixin", reason), + ("WASmaxOutGroupsSetSubjectChangeSubjectMixin", reason), + ] + ); // Only the small source capture has this exact catalog; the full artifact // legitimately contains additional operations. let mut modules: Vec<_> = ir["stanzas"] @@ -426,3 +458,71 @@ fn captured_oracle_rejects_extra_or_duplicate_stanzas() { } } } + +#[test] +fn response_oracle_rejects_spurious_outer_guards() { + let ir = captured(VERSIONS[1]); + for name in ["SetSubject", "AcceptGroupAdd"] { + let mut changed = ir.clone(); + let op = changed["stanzas"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|s| s["moduleName"] == format!("WASmaxOutGroups{name}Request")) + .unwrap(); + op["response"]["assertions"] = + json!([{"kind":"attr", "name":"unreviewed", "value":"required"}]); + assert!( + std::panic::catch_unwind(|| response_cases(&changed, false)).is_err(), + "accepted {name} outer guard" + ); + } +} + +#[test] +fn request_oracle_rejects_changed_parser_identities() { + let ir = captured(VERSIONS[1]); + for name in ["SetSubject", "AcceptGroupAdd"] { + for nested in [false, true] { + let mut changed = ir.clone(); + let op = changed["stanzas"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|s| s["moduleName"] == format!("WASmaxOutGroups{name}Request")) + .unwrap(); + if nested { + op["response"]["parserName"] = json!("unknown"); + } else { + op["parserName"] = json!("wrong"); + } + assert!( + std::panic::catch_unwind(|| request_contract(&changed)).is_err(), + "accepted {name} parser, nested={nested}" + ); + } + } +} + +#[test] +fn captured_oracle_rejects_changed_exclusion_diagnostics() { + for version in VERSIONS { + let ir = captured(version); + for mutation in ["missing", "reason", "extra", "duplicate"] { + let mut changed = ir.clone(); + let entries = changed["unparseable"].as_array_mut().unwrap(); + match mutation { + "missing" => { + entries.pop(); + } + "reason" => entries[0]["reason"] = json!("genuine failure"), + "duplicate" => entries.push(entries[0].clone()), + _ => entries.push(json!({"moduleName":"Unreviewed", "reason":"genuine failure"})), + } + assert!( + std::panic::catch_unwind(|| captured_contract(&changed)).is_err(), + "accepted {version} diagnostic {mutation}" + ); + } + } +} diff --git a/tests/conformance/README.md b/tests/conformance/README.md index bd4dc2a1..45c2d4e5 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -261,3 +261,12 @@ Presence is compared as a complete tree, including empty scalar `fields` and absent `items` for all nodes in this object-only capture. Nineteen structural mutations (children/items on each scalar and items on input) fail the preceding oracle's regression test and are rejected by the full-tree comparison. + +The IQ oracle pins both outer and nested RPC parser identities and the empty +outer assertion list: the captured RPC wrappers delegate guards to each outcome. +It also checks the exact three captured merge-mixin exclusions and their stable +fragment reason, keeping fragment diagnostics distinct from genuine failures. +This checks published extractor diagnostics against reviewed source roles; it +is not another server-protocol claim or a change to quality/diff/lint code. +Mutations of guards, parser names and missing/changed/extra/duplicate exclusions +fail the preceding oracle's regression tests and are rejected after correction.