Add EngineOverrides WebIDL AST pre-pass to js_interop_gen (#593)

Adds `EngineOverrides` (`js_interop_gen/lib/src/engine_overrides.dart`) as an isolated pre-translation WebIDL AST pass for reconciling `@webref/idl` against shipping Blink, Gecko, and WebKit WebIDL trees, with zero diff in `web/`:

- Rewrite `interfaces` (`kind: "interface"` to restore `dictionary` nodes rewritten by `w3c/webref` curation patches back to read-only `interface` nodes), `typedefs`, `properties`, and `operations` (`returnType`, `maxParameters`, `parameters`) in `EngineOverrides.applyToAst` before `_Library.add` or `elements.dart` run.
- Apply concrete-interface member overrides to shallow-cloned `interface mixin` member nodes during `Translator._combineInterfacelikes` (`EngineOverrides.applyToMixin`) while rejecting direct `interface mixin` targets in `validateTypes`.
- Suppress interfaces marked `generate: false` in `Translator._shouldGenerateInterface` after `BrowserCompatData.shouldGenerateInterface`, validate override target types via `Translator.resolveIdlTypeName`, and fail loudly via `StateError` in `verifyAllUsed` when any override in `engine_overrides.json` becomes unmatched or redundant.
- Read `revisions` from `engine_overrides.json` in `web_generator/bin/update_idl_bindings.dart` to render the pinned browser engine baseline table in `web_generator/README.md`, and document the override roll workflow.
diff --git a/js_interop_gen/lib/src/dart_main.dart b/js_interop_gen/lib/src/dart_main.dart
index 7200872..9583f91 100644
--- a/js_interop_gen/lib/src/dart_main.dart
+++ b/js_interop_gen/lib/src/dart_main.dart
@@ -13,6 +13,7 @@
 import 'package:yaml/yaml.dart';
 
 import 'config.dart';
+import 'engine_overrides.dart';
 import 'generate_bindings.dart';
 import 'interop_gen/parser.dart';
 import 'interop_gen/transform.dart';
@@ -48,6 +49,7 @@
       languageVersion: Version.parse(languageVersionString),
       idlJsonPath: argResult['idl-json'] as String?,
       bcdJsonPath: bcdJsonPath,
+      engineOverridesPath: argResult['engine-overrides'] as String?,
     );
   } else if (argResult.wasParsed('declaration')) {
     final Config config;
@@ -130,6 +132,7 @@
   required Version languageVersion,
   String? idlJsonPath,
   required String bcdJsonPath,
+  String? engineOverridesPath,
 }) async {
   if (input == null) {
     // parse dom library as normal
@@ -139,6 +142,8 @@
 
     final dir = p.dirname(p.fromUri(url));
     final renameMapPath = p.join(dir, 'web_rename_map.json');
+    EngineOverrides.activePath =
+        engineOverridesPath ?? p.join(dir, 'engine_overrides.json');
     final (bindings, renameMap) = await generateBindings(
       packageRoot,
       librarySubDir,
@@ -164,6 +169,7 @@
     final allInputFiles = expandGlobs(input.toList(), extension: '.idl');
     // parse individual files
     ensureDirectoryExists(output);
+    EngineOverrides.activePath = engineOverridesPath;
 
     final bindings = await generateBindingsForFiles(
       {
@@ -250,4 +256,8 @@
   ..addOption(
     'bcd-json',
     help: 'Path to the browser-compat-data data.json file',
+  )
+  ..addOption(
+    'engine-overrides',
+    help: 'Path to the engine_overrides.json file',
   );
diff --git a/js_interop_gen/lib/src/engine_overrides.dart b/js_interop_gen/lib/src/engine_overrides.dart
new file mode 100644
index 0000000..6b73b32
--- /dev/null
+++ b/js_interop_gen/lib/src/engine_overrides.dart
@@ -0,0 +1,583 @@
+// Copyright (c) 2026, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:js_interop';
+import 'dart:js_interop_unsafe';
+
+import 'js/filesystem_api.dart';
+import 'js/webidl_api.dart' as idl;
+import 'type_aliases.dart';
+
+@JS('Object.defineProperty')
+external void _defineProperty(
+  JSObject target,
+  JSString prop,
+  JSObject descriptor,
+);
+
+@JS('Object.getPrototypeOf')
+external JSObject? _getPrototypeOf(JSObject target);
+
+@JS('Object.getOwnPropertyDescriptors')
+external JSObject _getOwnPropertyDescriptors(JSObject target);
+
+@JS('Object.create')
+external JSObject _objectCreate(JSObject? proto, JSObject descriptors);
+
+JSObject _cloneJsObject(JSObject obj) =>
+    _objectCreate(_getPrototypeOf(obj), _getOwnPropertyDescriptors(obj));
+
+idl.Node _cloneMemberForOverride(idl.Node member) {
+  final cloned = _cloneJsObject(member as JSObject) as idl.Node;
+  if (member.type == 'operation') {
+    final args = (member as idl.Operation).arguments;
+    final clonedArgs = <idl.Argument>[
+      for (var i = 0; i < args.length; i++)
+        _cloneJsObject(args[i] as JSObject) as idl.Argument,
+    ].toJS;
+    _setJsProperty(cloned as JSObject, 'arguments', clonedArgs);
+  }
+  return cloned;
+}
+
+void _setJsProperty(JSObject target, String prop, JSAny? value) {
+  final desc = JSObject()
+    ..setProperty('value'.toJS, value)
+    ..setProperty('writable'.toJS, true.toJS)
+    ..setProperty('configurable'.toJS, true.toJS)
+    ..setProperty('enumerable'.toJS, true.toJS);
+  _defineProperty(target, prop.toJS, desc);
+}
+
+bool _rawIdlMatchesOverride(
+  idl.IDLType idlType,
+  EngineTypeOverride override, {
+  bool? rawRequired,
+}) {
+  if (override.required != null &&
+      rawRequired != null &&
+      override.required != rawRequired) {
+    return false;
+  }
+  if (idlType.union || idlType.nullable != override.nullable) return false;
+  if (idlType.generic.isNotEmpty) {
+    if (idlType.generic != override.type || override.typeParameter == null) {
+      return false;
+    }
+    final types = (idlType.idlType as JSArray<idl.IDLType>).toDart;
+    return types.length == 1 &&
+        _rawIdlMatchesOverride(types[0], override.typeParameter!);
+  }
+  if (override.typeParameter != null) return false;
+  final rawName = (idlType.idlType as JSString).toDart;
+  final rawAlias = idlOrBuiltinToJsTypeAliases[rawName] ?? rawName;
+  final overrideAlias =
+      idlOrBuiltinToJsTypeAliases[override.type] ?? override.type;
+  return rawAlias == overrideAlias;
+}
+
+idl.IDLType _createIdlType(EngineTypeOverride override) {
+  final obj = JSObject();
+  obj.setProperty('type'.toJS, ''.toJS);
+  obj.setProperty('extAttrs'.toJS, <JSObject>[].toJS);
+  obj.setProperty('union'.toJS, false.toJS);
+  obj.setProperty('nullable'.toJS, override.nullable.toJS);
+  if (override.typeParameter != null) {
+    obj.setProperty('generic'.toJS, override.type.toJS);
+    obj.setProperty(
+      'idlType'.toJS,
+      <idl.IDLType>[_createIdlType(override.typeParameter!)].toJS,
+    );
+  } else {
+    obj.setProperty('generic'.toJS, ''.toJS);
+    obj.setProperty('idlType'.toJS, override.type.toJS);
+  }
+  return obj as idl.IDLType;
+}
+
+void _convertDictionaryToReadonlyInterface(idl.Interfacelike interfacelike) {
+  _setJsProperty(interfacelike as JSObject, 'type', 'interface'.toJS);
+  final members = interfacelike.members;
+  for (var m = 0; m < members.length; m++) {
+    final member = members[m];
+    if (member.type == 'field') {
+      final memberObj = member as JSObject;
+      _setJsProperty(memberObj, 'type', 'attribute'.toJS);
+      _setJsProperty(memberObj, 'readonly', true.toJS);
+      _setJsProperty(memberObj, 'special', ''.toJS);
+      _setJsProperty(memberObj, 'inherit', false.toJS);
+    }
+  }
+}
+
+void _validateTargetNotMixin(
+  Map<String, idl.Node> typeToDeclaration,
+  String key,
+  String targetName,
+) {
+  if (typeToDeclaration[targetName]?.type == 'interface mixin') {
+    throw StateError(
+      'Engine override "$key" cannot target interface mixin "$targetName"; '
+      'target the concrete interface that includes it instead.',
+    );
+  }
+}
+
+void _validateTypeOverride(
+  Map<String, idl.Node> typeToDeclaration,
+  String Function(String) resolveIdlTypeName,
+  EngineTypeOverride override,
+) {
+  if (override.typeParameter != null) {
+    _validateTypeOverride(
+      typeToDeclaration,
+      resolveIdlTypeName,
+      override.typeParameter!,
+    );
+  }
+  final type = resolveIdlTypeName(override.type);
+  final decl = typeToDeclaration[type];
+  final alias = idlOrBuiltinToJsTypeAliases[type];
+  if (decl == null && alias == null) {
+    throw StateError('Unknown IDL override type: $type');
+  }
+}
+
+/// Declarative overrides reconciled from the Blink (Chrome), Gecko (Firefox),
+/// and WebKit (Safari) browser engine WebIDL trees where `>= 2` shipping
+/// engines disagree with `@webref/idl`.
+class EngineOverrides {
+  static String? activePath;
+
+  final Map<String, EngineInterfaceOverride> interfaces;
+  final Map<String, EngineTypeOverride> typedefs;
+  final Map<String, EngineTypeOverride> properties;
+  final Map<String, EngineOperationOverride> operations;
+
+  // Override maps remain intact across translation because entries can match
+  // multiple times (repeated type references, partial interfaces, overloads)
+  // and are validated after `applyToAst` in `validateTypes`.
+  final Set<String> _seenInterfacesInAst = {};
+  final Set<String> _usedInterfaces = {};
+  final Set<String> _usedTypedefs = {};
+  final Set<String> _usedProperties = {};
+  final Set<String> _usedOperations = {};
+  final Set<String> _matchedSubKeys = {};
+  final Set<String> _changedSubKeys = {};
+
+  EngineOverrides({
+    required this.interfaces,
+    required this.typedefs,
+    required this.properties,
+    required this.operations,
+  });
+
+  static EngineOverrides? read(String? path) {
+    if (path == null || !fs.existsSync(path.toJS).toDart) return null;
+    final content = (fs.readFileSync(
+      path.toJS,
+      JSReadFileOptions(encoding: 'utf8'.toJS),
+    ) as JSString).toDart;
+    final json = jsonDecode(content) as Map<String, dynamic>;
+    return EngineOverrides.fromJson(json);
+  }
+
+  factory EngineOverrides.fromJson(Map<String, dynamic> json) {
+    final interfacesJson =
+        (json['interfaces'] as Map?)?.cast<String, dynamic>() ?? const {};
+    final typedefsJson =
+        (json['typedefs'] as Map?)?.cast<String, dynamic>() ?? const {};
+    final propertiesJson =
+        (json['properties'] as Map?)?.cast<String, dynamic>() ?? const {};
+    final operationsJson =
+        (json['operations'] as Map?)?.cast<String, dynamic>() ?? const {};
+
+    return EngineOverrides(
+      interfaces: {
+        for (final entry in interfacesJson.entries)
+          entry.key: EngineInterfaceOverride.fromJson(
+            entry.value as Map<String, dynamic>,
+          ),
+      },
+      typedefs: {
+        for (final entry in typedefsJson.entries)
+          entry.key: EngineTypeOverride.fromJson(
+            entry.value as Map<String, dynamic>,
+          ),
+      },
+      properties: {
+        for (final entry in propertiesJson.entries)
+          entry.key: EngineTypeOverride.fromJson(
+            entry.value as Map<String, dynamic>,
+          ),
+      },
+      operations: {
+        for (final entry in operationsJson.entries)
+          entry.key: EngineOperationOverride.fromJson(
+            entry.value as Map<String, dynamic>,
+          ),
+      },
+    );
+  }
+
+  /// Mutates the parsed `webidl2` [ast] nodes in place before translation.
+  void applyToAst(JSArray<idl.Node> ast) {
+    for (var i = 0; i < ast.length; i++) {
+      final node = ast[i];
+      switch (node.type) {
+        case 'typedef':
+          _applyTypedefOverride(node as idl.Typedef);
+        case 'interface':
+        case 'dictionary':
+        case 'namespace':
+          _applyInterfacelikeOverride(node as idl.Interfacelike);
+      }
+    }
+  }
+
+  void _applyTypedefOverride(idl.Typedef typedefNode) {
+    final name = typedefNode.name;
+    final override = typedefs[name];
+    if (override == null) return;
+    _usedTypedefs.add(name);
+    final subKey = 'typedefs.$name';
+    _matchedSubKeys.add(subKey);
+    if (!_rawIdlMatchesOverride(typedefNode.idlType, override)) {
+      _changedSubKeys.add(subKey);
+      _setJsProperty(
+        typedefNode as JSObject,
+        'idlType',
+        _createIdlType(override),
+      );
+    }
+  }
+
+  void _applyInterfacelikeOverride(idl.Interfacelike interfacelike) {
+    final interfaceName = interfacelike.name;
+    final ifaceOverride = interfaces[interfaceName];
+    if (!interfacelike.partial) {
+      _seenInterfacesInAst.add(interfaceName);
+      if (ifaceOverride != null && ifaceOverride.kind != null) {
+        _usedInterfaces.add(interfaceName);
+        final subKey = 'interfaces.$interfaceName';
+        _matchedSubKeys.add(subKey);
+        if (interfacelike.type != ifaceOverride.kind) {
+          _changedSubKeys.add(subKey);
+        }
+      }
+    }
+    if (ifaceOverride?.kind == 'interface' &&
+        interfacelike.type == 'dictionary') {
+      _convertDictionaryToReadonlyInterface(interfacelike);
+    }
+    _applyMemberOverrides(interfaceName, interfacelike.members);
+  }
+
+  /// Applies any member overrides targeting [targetInterfaceName] to a shallow
+  /// clone of [mixin] so other interfaces including [mixin] stay untouched.
+  idl.Interfacelike applyToMixin(
+    String targetInterfaceName,
+    idl.Interfacelike mixin,
+  ) {
+    final members = mixin.members;
+    if (!_hasMemberOverridesFor(targetInterfaceName, members)) return mixin;
+    final clonedMixin = _cloneJsObject(mixin as JSObject) as idl.Interfacelike;
+    final clonedMembers = <idl.Node>[
+      for (var m = 0; m < members.length; m++)
+        _cloneMemberForOverride(members[m]),
+    ].toJS;
+    _setJsProperty(clonedMixin as JSObject, 'members', clonedMembers);
+    _applyMemberOverrides(targetInterfaceName, clonedMembers);
+    return clonedMixin;
+  }
+
+  bool _hasMemberOverridesFor(String interfaceName, JSArray<idl.Node> members) {
+    for (var m = 0; m < members.length; m++) {
+      final member = members[m];
+      switch (member.type) {
+        case 'attribute':
+        case 'field':
+          if (properties.containsKey('$interfaceName.${member.name}')) {
+            return true;
+          }
+        case 'operation':
+          final memberName = (member as idl.Operation).name;
+          if (memberName.isNotEmpty &&
+              operations.containsKey('$interfaceName.$memberName')) {
+            return true;
+          }
+      }
+    }
+    return false;
+  }
+
+  void _applyMemberOverrides(String interfaceName, JSArray<idl.Node> members) {
+    for (var m = 0; m < members.length; m++) {
+      final member = members[m];
+      switch (member.type) {
+        case 'attribute':
+        case 'field':
+          _applyPropertyOverride(interfaceName, member);
+        case 'operation':
+        case 'constructor':
+          _applyOperationOverride(interfaceName, member);
+      }
+    }
+  }
+
+  void _applyPropertyOverride(String interfaceName, idl.Node member) {
+    final isField = member.type == 'field';
+    final key = '$interfaceName.${member.name}';
+    final propOverride = properties[key];
+    if (propOverride == null) return;
+    _usedProperties.add(key);
+    final subKey = 'properties.$key';
+    _matchedSubKeys.add(subKey);
+    final rawIdlType = isField
+        ? (member as idl.Field).idlType
+        : (member as idl.Attribute).idlType;
+    final rawRequired = isField ? (member as idl.Field).required : null;
+    if (!_rawIdlMatchesOverride(
+      rawIdlType,
+      propOverride,
+      rawRequired: rawRequired,
+    )) {
+      _changedSubKeys.add(subKey);
+    }
+    final memberObj = member as JSObject;
+    _setJsProperty(memberObj, 'idlType', _createIdlType(propOverride));
+    if (isField && propOverride.required != null) {
+      _setJsProperty(memberObj, 'required', propOverride.required!.toJS);
+    }
+  }
+
+  void _applyOperationOverride(String interfaceName, idl.Node member) {
+    final isConstructor = member.type == 'constructor';
+    final memberName = isConstructor
+        ? 'constructor'
+        : (member as idl.Operation).name;
+    if (memberName.isEmpty) return;
+    final key = '$interfaceName.$memberName';
+    final opOverride = operations[key];
+    if (opOverride == null) return;
+    _usedOperations.add(key);
+    if (!isConstructor && opOverride.returnType != null) {
+      final operation = member as idl.Operation;
+      final subKey = 'operations.$key.returnType';
+      _matchedSubKeys.add(subKey);
+      if (!_rawIdlMatchesOverride(operation.idlType, opOverride.returnType!)) {
+        _changedSubKeys.add(subKey);
+      }
+      _setJsProperty(
+        member as JSObject,
+        'idlType',
+        _createIdlType(opOverride.returnType!),
+      );
+    }
+    final arguments = isConstructor
+        ? (member as idl.Constructor).arguments
+        : (member as idl.Operation).arguments;
+    final maxParams = opOverride.maxParameters;
+    if (maxParams != null) {
+      final subKey = 'operations.$key';
+      _matchedSubKeys.add(subKey);
+      if (arguments.length > maxParams) {
+        _changedSubKeys.add(subKey);
+        arguments.length = maxParams;
+      }
+    }
+    _applyParameterOverrides(key, arguments, opOverride.parameters);
+  }
+
+  void _applyParameterOverrides(
+    String key,
+    JSArray<idl.Argument> arguments,
+    Map<int, EngineTypeOverride> parameters,
+  ) {
+    for (final paramEntry in parameters.entries) {
+      final paramIndex = paramEntry.key;
+      final paramOverride = paramEntry.value;
+      if (paramIndex >= arguments.length) continue;
+      final subKey = 'operations.$key.parameters.$paramIndex';
+      _matchedSubKeys.add(subKey);
+      final arg = arguments[paramIndex];
+      if (!_rawIdlMatchesOverride(arg.idlType, paramOverride)) {
+        _changedSubKeys.add(subKey);
+      }
+      final argObj = arg as JSObject;
+      _setJsProperty(argObj, 'idlType', _createIdlType(paramOverride));
+      if (paramOverride.name != null) {
+        _setJsProperty(argObj, 'name', paramOverride.name!.toJS);
+      }
+    }
+  }
+
+  bool? shouldGenerateInterface(String name) {
+    final override = interfaces[name];
+    if (override == null) return null;
+    if (_seenInterfacesInAst.contains(name)) {
+      _usedInterfaces.add(name);
+    }
+    return override.generate;
+  }
+
+  /// Validates that overrides never target an `interface mixin` directly and
+  /// that all IDL types referenced by overrides resolve via
+  /// [resolveIdlTypeName] to an entry in [typeToDeclaration] or
+  /// [idlOrBuiltinToJsTypeAliases].
+  void validateTypes(
+    Map<String, idl.Node> typeToDeclaration,
+    String Function(String) resolveIdlTypeName,
+  ) {
+    for (final name in interfaces.keys) {
+      _validateTargetNotMixin(typeToDeclaration, 'interfaces.$name', name);
+    }
+    for (final override in typedefs.values) {
+      _validateTypeOverride(typeToDeclaration, resolveIdlTypeName, override);
+    }
+    for (final entry in properties.entries) {
+      _validateTargetNotMixin(
+        typeToDeclaration,
+        'properties.${entry.key}',
+        entry.key.split('.').first,
+      );
+      _validateTypeOverride(typeToDeclaration, resolveIdlTypeName, entry.value);
+    }
+    for (final entry in operations.entries) {
+      _validateTargetNotMixin(
+        typeToDeclaration,
+        'operations.${entry.key}',
+        entry.key.split('.').first,
+      );
+      final override = entry.value;
+      if (override.returnType != null) {
+        _validateTypeOverride(
+          typeToDeclaration,
+          resolveIdlTypeName,
+          override.returnType!,
+        );
+      }
+      for (final param in override.parameters.values) {
+        _validateTypeOverride(typeToDeclaration, resolveIdlTypeName, param);
+      }
+    }
+  }
+
+  /// Verifies that every override in `engine_overrides.json` matched an active
+  /// IDL declaration during `package:web` generation and is not redundant.
+  void verifyAllUsed() {
+    final unused = <String>[
+      ...interfaces.keys
+          .toSet()
+          .difference(_usedInterfaces)
+          .map((k) => 'interfaces.$k'),
+      ...typedefs.keys
+          .toSet()
+          .difference(_usedTypedefs)
+          .map((k) => 'typedefs.$k'),
+      ...properties.keys
+          .toSet()
+          .difference(_usedProperties)
+          .map((k) => 'properties.$k'),
+      ...operations.keys
+          .toSet()
+          .difference(_usedOperations)
+          .map((k) => 'operations.$k'),
+      for (final key in _usedOperations)
+        ...operations[key]!.parameters.keys
+            .map((i) => 'operations.$key.parameters.$i')
+            .toSet()
+            .difference(_matchedSubKeys),
+    ];
+    if (unused.isNotEmpty) {
+      throw StateError(
+        'Stale engine overrides in engine_overrides.json (not matched during '
+        'IDL translation):\n  - ${unused.join('\n  - ')}',
+      );
+    }
+    final redundant = _matchedSubKeys.difference(_changedSubKeys).toList()
+      ..sort();
+    if (redundant.isNotEmpty) {
+      throw StateError(
+        'Redundant engine overrides in engine_overrides.json (@webref/idl now '
+        'natively matches the override; remove from engine_overrides.json):\n'
+        '  - ${redundant.join('\n  - ')}',
+      );
+    }
+  }
+}
+
+class EngineInterfaceOverride {
+  final bool generate;
+  final String? kind;
+
+  EngineInterfaceOverride({required this.generate, this.kind});
+
+  factory EngineInterfaceOverride.fromJson(Map<String, dynamic> json) =>
+      EngineInterfaceOverride(
+        generate: json['generate'] as bool? ?? true,
+        kind: json['kind'] as String?,
+      );
+}
+
+class EngineTypeOverride {
+  final String type;
+  final bool nullable;
+  final EngineTypeOverride? typeParameter;
+  final String? name;
+  final bool? required;
+
+  EngineTypeOverride({
+    required this.type,
+    required this.nullable,
+    this.typeParameter,
+    this.name,
+    this.required,
+  });
+
+  factory EngineTypeOverride.fromJson(Map<String, dynamic> json) =>
+      EngineTypeOverride(
+        type: json['type'] as String,
+        nullable: json['nullable'] as bool? ?? false,
+        typeParameter: json['typeParameter'] != null
+            ? EngineTypeOverride.fromJson(
+                json['typeParameter'] as Map<String, dynamic>,
+              )
+            : null,
+        name: json['name'] as String?,
+        required: json['required'] as bool?,
+      );
+}
+
+class EngineOperationOverride {
+  final EngineTypeOverride? returnType;
+  final Map<int, EngineTypeOverride> parameters;
+  final int? maxParameters;
+
+  EngineOperationOverride({
+    this.returnType,
+    this.parameters = const {},
+    this.maxParameters,
+  });
+
+  factory EngineOperationOverride.fromJson(Map<String, dynamic> json) {
+    final paramsJson =
+        (json['parameters'] as Map?)?.cast<String, dynamic>() ?? const {};
+    return EngineOperationOverride(
+      returnType: json['returnType'] != null
+          ? EngineTypeOverride.fromJson(
+              json['returnType'] as Map<String, dynamic>,
+            )
+          : null,
+      parameters: {
+        for (final entry in paramsJson.entries)
+          int.parse(entry.key): EngineTypeOverride.fromJson(
+            entry.value as Map<String, dynamic>,
+          ),
+      },
+      maxParameters: json['maxParameters'] as int?,
+    );
+  }
+}
diff --git a/js_interop_gen/lib/src/engine_overrides.json b/js_interop_gen/lib/src/engine_overrides.json
new file mode 100644
index 0000000..fc6aab5
--- /dev/null
+++ b/js_interop_gen/lib/src/engine_overrides.json
@@ -0,0 +1,7 @@
+{
+  "revisions": {},
+  "interfaces": {},
+  "typedefs": {},
+  "properties": {},
+  "operations": {}
+}
diff --git a/js_interop_gen/lib/src/translator.dart b/js_interop_gen/lib/src/translator.dart
index 6b28bdd..f3c6e56 100644
--- a/js_interop_gen/lib/src/translator.dart
+++ b/js_interop_gen/lib/src/translator.dart
@@ -11,6 +11,7 @@
 import 'bcd.dart';
 import 'doc_provider.dart';
 import 'elements.dart';
+import 'engine_overrides.dart';
 import 'formatting.dart';
 import 'js/webidl_api.dart' as idl;
 import 'js/webref_elements_api.dart';
@@ -167,6 +168,7 @@
   late String _currentlyTranslatingUrl;
   late DocProvider docProvider;
   late BrowserCompatData browserCompatData;
+  late final EngineOverrides? engineOverrides;
 
   /// Singleton so that various helper methods can access info about the AST.
   static Translator? instance;
@@ -187,8 +189,13 @@
       generateAll: generateAll,
       path: bcdJsonPath,
     );
+    engineOverrides = EngineOverrides.read(EngineOverrides.activePath);
   }
 
+  bool _shouldGenerateInterface(String name) =>
+      browserCompatData.shouldGenerateInterface(name) &&
+      (engineOverrides?.shouldGenerateInterface(name) ?? true);
+
   void _addOrUpdateInterfaceLike(idl.Interfacelike interfacelike) {
     final name = interfacelike.name;
     if (_interfacelikes.containsKey(name)) {
@@ -214,6 +221,7 @@
   /// [markTypeAsUsed] because they don't have any compat data and are emitted
   /// only if used.
   void addInterfacesAndNamespaces() {
+    engineOverrides?.validateTypes(_typeToDeclaration, resolveIdlTypeName);
     for (final library in _libraries.values) {
       for (final interfacelike in library.interfacelikes) {
         final name = interfacelike.name;
@@ -256,7 +264,10 @@
         _typeToDeclaration[mixin] as idl.Interfacelike,
         ...?_typeToPartials[mixin],
       ]) {
-        _interfacelikes[interfacelikeName]!.update(interfacelike);
+        _interfacelikes[interfacelikeName]!.update(
+          engineOverrides?.applyToMixin(interfacelikeName, interfacelike) ??
+              interfacelike,
+        );
       }
     }
   }
@@ -297,7 +308,7 @@
         // Interfaces and namespaces can only be marked as used depending on
         // their compat data.
         final name = (decl as idl.Interfacelike).name;
-        if (browserCompatData.shouldGenerateInterface(name)) {
+        if (_shouldGenerateInterface(name)) {
           _usedTypes.add(decl);
           _combineInterfacelikes(name);
           return true;
@@ -309,7 +320,7 @@
         // https://github.com/mdn/browser-compat-data/blob/main/docs/data-guidelines/api.md#namespaces
         final namespace = decl as idl.Interfacelike;
         final name = namespace.name;
-        if (browserCompatData.shouldGenerateInterface(name) ||
+        if (_shouldGenerateInterface(name) ||
             namespace.members.toDart.every(
               (member) => member.type == 'const',
             )) {
@@ -331,6 +342,8 @@
     final libraryPath = '$_librarySubDir/${shortName.kebabToSnake}.dart';
     assert(!_libraries.containsKey(libraryPath));
 
+    engineOverrides?.applyToAst(ast);
+
     final library = _Library(shortName, '${packageRoot ?? '.'}/$libraryPath');
 
     for (var i = 0; i < ast.length; i++) {
@@ -1289,7 +1302,12 @@
     }
 
     if (_generateForWeb) {
+      if (!browserCompatData.generateAll) {
+        engineOverrides?.verifyAllUsed();
+      }
       dartLibraries['dom.dart'] = generateRootImport(dartLibraries.keys);
+    } else {
+      engineOverrides?.verifyAllUsed();
     }
 
     return dartLibraries;
diff --git a/js_interop_gen/test/engine_overrides_test.dart b/js_interop_gen/test/engine_overrides_test.dart
new file mode 100644
index 0000000..270781d
--- /dev/null
+++ b/js_interop_gen/test/engine_overrides_test.dart
@@ -0,0 +1,331 @@
+// Copyright (c) 2026, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:js_interop_gen/src/cli.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import 'test_shared.dart';
+
+void main() {
+  group('EngineOverrides AST pre-pass', () {
+    late Directory tempDir;
+    late String idlPath;
+    late String bcdPath;
+
+    const sampleIdl = '''
+interface DroppedInterface {
+  attribute DOMString ignored;
+};
+
+interface mixin SharedScrollMixin {
+  readonly attribute long mixinOffset;
+  Promise<undefined> mixinAction(long step);
+};
+
+dictionary ReportBodyDict {
+  USVString documentURL;
+  unsigned short statusCode;
+};
+
+typedef (unsigned long or bigint) AddressValue;
+
+dictionary SampleInit {
+  long clientX = 0;
+  unsigned long? length;
+};
+
+interface SampleElement {
+  readonly attribute DroppedInterface? dropped;
+  readonly attribute long clientX;
+  attribute AddressValue address;
+  Promise<undefined> scroll();
+  Promise<undefined> scroll(long x, optional boolean extra);
+  Promise<undefined> generateKeyFrame();
+};
+SampleElement includes SharedScrollMixin;
+
+interface MixinPeer {
+};
+MixinPeer includes SharedScrollMixin;
+''';
+
+    setUpAll(() async {
+      await compileBindingsGen();
+      tempDir = await Directory.systemTemp.createTemp('engine_overrides_test_');
+      idlPath = p.join(tempDir.path, 'sample.idl');
+      bcdPath = p.join(tempDir.path, 'dummy_bcd.json');
+      await File(idlPath).writeAsString(sampleIdl);
+      await File(bcdPath)
+          .writeAsString('{"api": {}, "webassembly": {"api": {}}}');
+    });
+
+    tearDownAll(() async {
+      if (await tempDir.exists()) {
+        await tempDir.delete(recursive: true);
+      }
+    });
+
+    test(
+      'applies interface, typedef, property, and operation overrides',
+      () async {
+        final overridesPath = p.join(tempDir.path, 'valid_overrides.json');
+        await File(overridesPath).writeAsString(
+          jsonEncode({
+            'interfaces': {
+              'DroppedInterface': {
+                'generate': false,
+                'reason': 'Not shipped in >=2 engines.',
+              },
+              'ReportBodyDict': {
+                'generate': true,
+                'kind': 'interface',
+                'reason': 'Shipped as read-only interface in all 3 engines.',
+              },
+            },
+            'typedefs': {
+              'AddressValue': {'type': 'unsigned long', 'nullable': false},
+            },
+            'properties': {
+              'SampleInit.clientX': {'type': 'double', 'nullable': false},
+              'SampleInit.length': {
+                'type': 'unsigned long',
+                'nullable': false,
+                'required': true,
+              },
+              'SampleElement.clientX': {'type': 'double', 'nullable': false},
+              'SampleElement.mixinOffset': {
+                'type': 'double',
+                'nullable': false,
+              },
+            },
+            'operations': {
+              'SampleElement.scroll': {
+                'returnType': {'type': 'undefined', 'nullable': false},
+                'maxParameters': 1,
+                'parameters': {
+                  '0': {'name': 'coord', 'type': 'double', 'nullable': false},
+                },
+              },
+              'SampleElement.mixinAction': {
+                'returnType': {'type': 'undefined', 'nullable': false},
+                'parameters': {
+                  '0': {'type': 'double', 'nullable': false},
+                },
+              },
+              'SampleElement.generateKeyFrame': {
+                'returnType': {
+                  'type': 'Promise',
+                  'nullable': false,
+                  'typeParameter': {
+                    'type': 'unsigned long long',
+                    'nullable': false,
+                  },
+                },
+              },
+            },
+          }),
+        );
+
+        final outDir = p.join(tempDir.path, 'out_valid');
+        await Directory(outDir).create(recursive: true);
+
+        final relOverrides = p.relative(overridesPath, from: bindingsGenPath);
+        await runNode([
+          'main.mjs',
+          '--idl',
+          '--input=${p.relative(idlPath, from: bindingsGenPath)}',
+          '--output=${p.relative(outDir, from: bindingsGenPath)}',
+          '--bcd-json=${p.relative(bcdPath, from: bindingsGenPath)}',
+          '--engine-overrides=$relOverrides',
+        ], workingDirectory: bindingsGenPath);
+
+        final generated = await File(p.join(outDir, 'sample.dart'))
+            .readAsString();
+        expect(generated, isNot(contains('DroppedInterface')));
+        expect(generated, contains('external JSObject? get dropped;'));
+        expect(
+          generated,
+          contains('extension type ReportBodyDict._(JSObject _)'),
+        );
+        expect(generated, contains('external String get documentURL;'));
+        expect(generated, contains('external int get statusCode;'));
+        expect(generated, isNot(contains('set documentURL')));
+        expect(generated, isNot(contains('factory ReportBodyDict')));
+        expect(generated, contains('typedef AddressValue = int;'));
+        expect(
+          generated,
+          contains(
+            'external factory SampleInit({num clientX, required int length});',
+          ),
+        );
+        expect(generated, contains('external double get clientX;'));
+        expect(generated, contains('external void scroll([num coord]);'));
+        expect(
+          generated,
+          contains('external JSPromise<JSNumber> generateKeyFrame();'),
+        );
+        expect(generated, contains('external double get mixinOffset;'));
+        expect(generated, contains('external void mixinAction(num step);'));
+        // MixinPeer includes the same SharedScrollMixin without overrides, so
+        // its mixed-in members must retain their original WebIDL signatures.
+        expect(generated, contains('external int get mixinOffset;'));
+        expect(
+          generated,
+          contains('external JSPromise<JSAny?> mixinAction(int step);'),
+        );
+      },
+    );
+
+    test(
+      'throws StateError when override targets an interface mixin',
+      () async {
+        final overridesPath = p.join(tempDir.path, 'mixin_overrides.json');
+        await File(overridesPath).writeAsString(
+          jsonEncode({
+            'properties': {
+              'SharedScrollMixin.mixinOffset': {
+                'type': 'double',
+                'nullable': false,
+              },
+            },
+          }),
+        );
+
+        final outDir = p.join(tempDir.path, 'out_mixin_target');
+        await Directory(outDir).create(recursive: true);
+
+        final relOverrides = p.relative(overridesPath, from: bindingsGenPath);
+        final process = await runNodeWithResult([
+          'main.mjs',
+          '--idl',
+          '--input=${p.relative(idlPath, from: bindingsGenPath)}',
+          '--output=${p.relative(outDir, from: bindingsGenPath)}',
+          '--bcd-json=${p.relative(bcdPath, from: bindingsGenPath)}',
+          '--engine-overrides=$relOverrides',
+        ], workingDirectory: bindingsGenPath);
+
+        final stderr = (await process.stderr.transform(utf8.decoder).toList())
+            .join();
+        expect(await process.exitCode, isNot(0));
+        expect(
+          stderr,
+          contains('cannot target interface mixin "SharedScrollMixin"'),
+        );
+      },
+    );
+
+    test('throws StateError on stale override', () async {
+      final overridesPath = p.join(tempDir.path, 'stale_overrides.json');
+      await File(overridesPath).writeAsString(
+        jsonEncode({
+          'properties': {
+            'SampleElement.nonExistent': {'type': 'double', 'nullable': false},
+          },
+        }),
+      );
+
+      final outDir = p.join(tempDir.path, 'out_stale');
+      await Directory(outDir).create(recursive: true);
+
+      final relOverrides = p.relative(overridesPath, from: bindingsGenPath);
+      final process = await runNodeWithResult([
+        'main.mjs',
+        '--idl',
+        '--input=${p.relative(idlPath, from: bindingsGenPath)}',
+        '--output=${p.relative(outDir, from: bindingsGenPath)}',
+        '--bcd-json=${p.relative(bcdPath, from: bindingsGenPath)}',
+        '--engine-overrides=$relOverrides',
+      ], workingDirectory: bindingsGenPath);
+
+      final stderr = (await process.stderr.transform(utf8.decoder).toList())
+          .join();
+      expect(await process.exitCode, isNot(0));
+      expect(
+        stderr,
+        contains('Stale engine overrides in engine_overrides.json'),
+      );
+      expect(stderr, contains('properties.SampleElement.nonExistent'));
+    });
+
+    test('throws StateError on redundant override', () async {
+      final overridesPath = p.join(tempDir.path, 'redundant_overrides.json');
+      await File(overridesPath).writeAsString(
+        jsonEncode({
+          'interfaces': {
+            'SampleElement': {'generate': true, 'kind': 'interface'},
+          },
+          'properties': {
+            'SampleElement.clientX': {'type': 'long', 'nullable': false},
+          },
+          'operations': {
+            'SampleElement.generateKeyFrame': {'maxParameters': 0},
+          },
+        }),
+      );
+
+      final outDir = p.join(tempDir.path, 'out_redundant');
+      await Directory(outDir).create(recursive: true);
+
+      final relOverrides = p.relative(overridesPath, from: bindingsGenPath);
+      final process = await runNodeWithResult([
+        'main.mjs',
+        '--idl',
+        '--input=${p.relative(idlPath, from: bindingsGenPath)}',
+        '--output=${p.relative(outDir, from: bindingsGenPath)}',
+        '--bcd-json=${p.relative(bcdPath, from: bindingsGenPath)}',
+        '--engine-overrides=$relOverrides',
+      ], workingDirectory: bindingsGenPath);
+
+      final stderr = (await process.stderr.transform(utf8.decoder).toList())
+          .join();
+      expect(await process.exitCode, isNot(0));
+      expect(
+        stderr,
+        contains('Redundant engine overrides in engine_overrides.json'),
+      );
+      expect(stderr, contains('interfaces.SampleElement'));
+      expect(stderr, contains('properties.SampleElement.clientX'));
+      expect(stderr, contains('operations.SampleElement.generateKeyFrame'));
+    });
+
+    test('throws StateError on unknown override type', () async {
+      final overridesPath = p.join(tempDir.path, 'unknown_type_overrides.json');
+      await File(overridesPath).writeAsString(
+        jsonEncode({
+          'properties': {
+            'SampleElement.clientX': {
+              'type': 'UnknownIdlType',
+              'nullable': false,
+            },
+          },
+        }),
+      );
+
+      final outDir = p.join(tempDir.path, 'out_unknown_type');
+      await Directory(outDir).create(recursive: true);
+
+      final relOverrides = p.relative(overridesPath, from: bindingsGenPath);
+      final process = await runNodeWithResult([
+        'main.mjs',
+        '--idl',
+        '--input=${p.relative(idlPath, from: bindingsGenPath)}',
+        '--output=${p.relative(outDir, from: bindingsGenPath)}',
+        '--bcd-json=${p.relative(bcdPath, from: bindingsGenPath)}',
+        '--engine-overrides=$relOverrides',
+      ], workingDirectory: bindingsGenPath);
+
+      final stderr = (await process.stderr.transform(utf8.decoder).toList())
+          .join();
+      expect(await process.exitCode, isNot(0));
+      expect(stderr, contains('Unknown IDL override type: UnknownIdlType'));
+    });
+  });
+}
diff --git a/web_generator/README.md b/web_generator/README.md
index 199315f..4818c8d 100644
--- a/web_generator/README.md
+++ b/web_generator/README.md
@@ -28,6 +28,23 @@
 Alternatively, manually edit `package.json` to use specific versions, and re-run
 `update_idl_bindings.dart`.
 
+### Browser engine overrides (`engine_overrides.json`)
+
+`@webref/idl` tracks latest Editor's Drafts and applies curation patches that
+occasionally diverge from what shipping browser engines (Blink, Gecko, WebKit)
+implement. `js_interop_gen/lib/src/engine_overrides.json` pins targeted AST and
+interface-visibility overrides before translation:
+
+1. **Self-pruning on roll**: `update_idl_bindings.dart` fails with a
+   `StateError` if any override in `engine_overrides.json` is unused or already
+   matches `@webref/idl` (having become a no-op). Delete those entries and
+   re-run.
+2. **Reviewing `web/tool/api.txt`**: After rolling `@webref/idl` or
+   `@mdn/browser-compat-data`, inspect `git diff web/tool/api.txt` for
+   unintended signature widenings (e.g., concrete types widened to `JSAny`,
+   `void` changed to `JSPromise` before engines ship it, or single-engine
+   interfaces enabled in BCD) and add an entry with a `"reason"` if needed.
+
 ### Public API golden
 
 `web/tool/api.txt` is a [`package:api_summary`](https://pub.dev/packages/api_summary)
diff --git a/web_generator/bin/update_idl_bindings.dart b/web_generator/bin/update_idl_bindings.dart
index edda04b..76117c6 100644
--- a/web_generator/bin/update_idl_bindings.dart
+++ b/web_generator/bin/update_idl_bindings.dart
@@ -170,6 +170,7 @@
     final cssVersion = _packageLockVersion(_webRefCss);
     final elementsVersion = _packageLockVersion(_webRefElements);
     final idlVersion = _packageLockVersion(_webRefIdl);
+    final engineSection = _engineBaselineSection();
     final versions =
         '''
 $_startComment
@@ -179,7 +180,7 @@
 | `$_webRefCss` | [$cssVersion](https://www.npmjs.com/package/$_webRefCss/v/$cssVersion) |
 | `$_webRefElements` | [$elementsVersion](https://www.npmjs.com/package/$_webRefElements/v/$elementsVersion) |
 | `$_webRefIdl` | [$idlVersion](https://www.npmjs.com/package/$_webRefIdl/v/$idlVersion) |
-''';
+$engineSection''';
 
     final newContent =
         sourceContent.substring(0, sourceContent.indexOf(_startComment)) +
@@ -215,6 +216,40 @@
   return webRefIdl['version'] as String;
 }
 
+String _engineBaselineSection() {
+  final overridesFile = File(
+    p.join(bindingsGeneratorPath, 'engine_overrides.json'),
+  );
+  final data =
+      jsonDecode(overridesFile.readAsStringSync()) as Map<String, dynamic>;
+  final revisions = data['revisions'] as Map<String, dynamic>?;
+  if (revisions == null || revisions.isEmpty) return '';
+  const engineLabels = {'blink': 'Blink', 'gecko': 'Gecko', 'webkit': 'WebKit'};
+  final buffer = StringBuffer()
+    ..writeln()
+    ..writeln(
+      'Browser engine WebIDL baselines '
+      '(`js_interop_gen/lib/src/engine_overrides.json`):',
+    )
+    ..writeln()
+    ..writeln('| Engine | Browser | Ref | Commit |')
+    ..writeln('| --- | --- | --- | --- |');
+  for (final entry in engineLabels.entries) {
+    final rev = revisions[entry.key] as Map<String, dynamic>;
+    final browser = rev['browser'] as String;
+    final version = rev['version'] as String;
+    final repo = rev['repo'] as String;
+    final ref = rev['ref'] as String;
+    final sha = rev['sha'] as String;
+    final shortSha = sha.substring(0, 10);
+    buffer.writeln(
+      '| ${entry.value} | $browser $version | `$ref` | '
+      '[`$shortSha`](https://github.com/$repo/commit/$sha) |',
+    );
+  }
+  return buffer.toString();
+}
+
 const _browserCompatData = '@mdn/browser-compat-data';
 const _webRefCss = '@webref/css';
 const _webRefElements = '@webref/elements';