[{"id":"A1","fact":"**CORRECTED 2026-09-09.** `list_level_actors` and `set_actor_transform` use the SAME convention on both sides: **`x` = pitch, `y` = yaw, `z` = roll**. A rotation read from one posts straight back to the other. `{pitch,yaw,roll}` are accepted as alternate keys on write and mean the same slots; the array form is `[pitch,yaw,roll]`. Two caveats, both measured: components are not bit-equal on return (`y:90` reads back `89.99999999999999`), and at pitch ±90 they renormalise entirely (`{pitch:90}` reads back `{x:90,y:180,z:180}` — same orientation, different numbers). Compare orientation or use a tolerance, never `==`.","source":"live, UE 5.3 probe port 8843, 2026-09-09: a 4×1×1 cube; `{x:0,y:90,z:0}` moved `get_actor_bounds` extent 200,50,50 → 50,200,50 (long axis swung HORIZONTALLY = yaw); `{x:90}` and `{pitch:90}` both gave 50,50,200 (tilted = pitch); `[0,90,0]` == `{y:90}`; `{x:20,y:35,z:50}` read back identical and reposting it verbatim left components and bounds unchanged. Source: `MifBridgeLevel.cpp:117` emits `Vec(Rot.Pitch, Rot.Yaw, Rot.Roll)`, `MifBridgeCommon.cpp:2097-2100` reads x→Pitch","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts"},{"id":"A8","fact":"`ReadRotatorField` accepts BOTH spellings per component and **the primary wins silently**: `{\"y\":0,\"yaw\":90}` applies 0 and discards the 90 with no error, because both names are on the accepted-key list so the typo guard passes it. This is the one silent-ignore that reader is otherwise hardened against. **[Reviewed 2026-09-21 against the code - STALE in part: disagreeing rotation spellings are refused (MifBridgeCommon.cpp:2312-2342)]**","source":"live, port 8843: that payload left the probe cube at extent 200,50,50, unrotated. Source: `MifBridgeCommon.cpp:2013-2014`, accepted list built at `:1983-1985`","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts","status":"superseded"},{"id":"A9","fact":"**The rotation grammar is not uniform across endpoints, and the unaccepted spelling is SILENTLY ZEROED rather than refused.** `set_actor_transform`, `spawn_actor_in_level`, `add_component`, `set_component_transform`, `add_socket`, `spawn_many` and `add_foliage_instances` take x/y/z OR pitch/yaw/roll OR `[p,y,r]`. `set_viewport_camera`, `capture_camera`, `spawn_actor_in_pie`, `add_sublevel`, `pie_load_level_instance` and `start_pie` take **x/y/z only**. `ui_scenario_start` takes **pitch/yaw/roll only**. `JNum` returns its default for an absent key and `RejectUnknownParams` vets only top-level keys, so nothing refuses the wrong spelling. **[Reviewed 2026-09-21 against the code - STALE in part: all read rotation through ReadRotatorField (MifBridgeCommon.cpp:2280-2302, 2423-2433); every rotation goes through ReadRotatorField: {x,y,z}, {pitch,yaw,roll} or [p,y,r]; unknown components and disagreeing spellings refused (MifBridgeCommon.cpp:2423-2433, 2326-2342); all seven read rotation through ReadRotatorField: x/y/z, pitch/yaw/roll and [p,y,r]; unknown components refused (MifBridgeCommon.cpp:2280-2302, 2423-2433)]**","source":"live, port 8843: `set_viewport_camera rotation {pitch:-20,yaw:45,roll:0}` returned ok:true and the camera read back {x:0,y:0,z:0}; the same call with {x:-20,y:45,z:0} read back {x:-20,y:45,z:0}. Source: `MifBridgeViewport.cpp:342`, `MifBridgeUIScenario.cpp:395`, `MifBridgeCommon.cpp:2132`","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts","status":"superseded"},{"id":"A12","fact":"**`name in view_layer.objects` is a LINEAR SCAN, not a hashed lookup.** Any loop that does it per object is therefore quadratic. `set(view_layer.objects.keys())` built once gives **identical** answers and is O(1) per test. Applies to the `bpy_prop_collection` string-membership form generally, not just view layers. **[Reviewed 2026-09-21 against the code - STALE in part: link_objects/unlink_objects build one set/index per call; timings not re-measured (ops_collection.py:395-402, 485-496)]**","source":"live, Blender 4.4.0 headless `--background --factory-startup`, 2026-09-09. The test alone, over all objects: **2000 → 0.0314 s; 5000 → 0.1913 s; 10000 → 0.7405 s; 20000 → 3.0584 s** — 4× the work for 2× the objects, and the set form over the same objects took 0.0004/0.0010/0.0023/0.0042 s (731× at 20000) while agreeing on **every** object at all four sizes. That 3.06 s was essentially all of `list_objects`' 3.2 s at 20000. Fixing five sites took `list_objects` 3.22 s → 0.083 s and `scene_info` 2.89 s → 0.022 s. Probe: scratchpad `viewlayer_lookup.py`; verification `verify_fix.py` compares row-by-row against the old expression as an oracle. **The same mechanism measured through three more ops, all on 4.4.0:** `list_collections` (which calls a per-object helper that scans every collection) went 0.026/0.105/0.419/1.695 s at 1000/2000/4000/8000 objects — exactly ×4.0 per doubling, three doublings — and **1.695 s → 0.009 s** once indexed; `link_objects` 0.012/0.050/0.234/1.042 s and `unlink_objects` 0.008/0.033/0.178/0.808 s at 500/1000/2000/4000 objects passed in one call (×4.1, ×4.7, ×4.4 — still unfixed, they mutate while they read). Probes: `measure_collections.py`, `measure_link.py`","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts","status":"superseded"},{"id":"A2","fact":"`spawn_actor_in_level` returns the new path nested at **`actor.actorPath`**; there is no top-level `actorPath`","source":"live probe: returned keys were `['actor','labelActual','labelRequested','note','ok']`","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts"},{"id":"A3","fact":"`get_actor_bounds` returns **`extent`**, not `boxExtent`","source":"live readback","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts"},{"id":"A5","fact":"Destructive endpoints need `confirm:true` and **return ok without it**: `remove_collision`, `delete_level_actor`, `delete_asset` (whose arg is `path`, not `objectPath`) **[Reviewed 2026-09-21 against the code - STALE in part: delete_asset, remove_collision and delete_level_actor refuse without confirm (MifBridgeAssetOps.cpp:138-141; MifBridgeCollision.cpp:368-370; MifBridgeLevel.cpp:684-686); each fails with an error naming confirm (MifBridgeLevel.cpp:684-688; MifBridgeCollision.cpp:368-370; MifBridgeAssetOps.cpp:138-141)]**","source":"live measurement, earlier session","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts","status":"superseded"},{"id":"A16","fact":"**`UEditorActorSubsystem::GetAllLevelActors()` and `ULevel::Actors` do not count the same things, and the gap is not small.** `list_level_actors` (which uses the subsystem) and `list_sublevels`' `persistent.actorCount` (which counts `Level->Actors`) disagreed 3 vs 9 on the same world in the same second. Neither is wrong; they answer different questions, and a caller comparing them will think actors vanished. **THE OPEN QUESTION IS ANSWERED, 2026-09-20, and the guess was two of six.** `UEditorActorSubsystem::GetAllLevelActors()` reads, IDENTICALLY in 5.3 and 5.8: a `TActorIterator` over `GetEditorWorld()` with `EActorIteratorFlags::SkipPendingKill`, keeping an actor only if `IsEditable()` AND `IsListedInSceneOutliner()` AND `!IsTemplate()` AND `!HasAnyFlags(RF_Transient)` AND `!FActorEditorUtils::IsABuilderBrush()` AND `!IsA(AWorldSettings::StaticClass())`. So the builder brush and WorldSettings are real but are the two SMALLEST filters; the ones that move the number on a real level are `IsEditable`, `IsListedInSceneOutliner` and `RF_Transient`. Two more conditions gate the whole thing: it iterates ACTIVE levels only, and the entire body is inside `EditorScriptingHelpers::CheckIfInEditorAndPIE()`, so it can return an EMPTY array rather than an error outside that context. `CountValidActors` over `Level->Actors` applies none of the six. **RE-MEASURED live on 5.8.2 the same day: 151 against 158, a gap of 7** - so the disagreement persists, but the original 3-vs-9 was a near-empty probe world making a fixed offset look like a 3x error. The gap is structural; its SIZE is content-dependent, being however many transient and non-outliner actors a level happens to hold. **[Reviewed 2026-09-21 against the code - STALE in part: trailing caveat stale; six filters confirmed (EditorActorSubsystem.cpp:342-357 (5.3)); the six filters are confirmed; the 'NOT YET CONFIRMED' caveat is stale (EditorActorSubsystem.cpp:369)]**","source":"live, 5.3 probe port 8843, 2026-09-09, one partitioned map with one spawned floor: `list_level_actors{}` returned `count:3, matched:3, truncated:false` while `list_sublevels{world:\"editor\"}` returned `persistent.actorCount: 9`. Sources: `MifBridgeLevel.cpp:262` (`Subsystem->GetAllLevelActors()`) and `MifBridgeStreaming.cpp:399-408` (`CountValidActors`, a bare `IsValid` over `Level->Actors`) called at :450 and :563 (`Out->SetBoolField(TEXT(\"isPartitioned\"), World->IsPartitionedWorld());` is :563). **NOT YET CONFIRMED and deliberately not asserted here:** which engine-side predicates make up the difference - `GetAllLevelActors`' filter set was not read from engine source, so \"WorldSettings and the builder brush\" is a guess and is recorded as one.","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts","status":"superseded"},{"id":"A6","fact":"`describe_endpoint` **OMITS `acceptedParams` entirely** when it cannot enumerate an endpoint's parameters - it is never an empty list. `d.get(\"acceptedParams\") or []` therefore turns *unknown* into *takes no parameters*, which is the opposite claim","source":"live, 5.7 probe port 8847: the 12 `kr_*` endpoints return `status params_not_declared`, `paramsSource none`, and no `acceptedParams` key at all, while 19 endpoints that genuinely take none return `acceptedParams: []` WITH `acceptsNoParameters: true`","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts"},{"id":"A23","fact":"**`describe_property`'s `class` form deliberately suppresses the `default` column, and that suppression is CORRECT** - the form reads the class default object, so `value` IS the default and a `default` column would duplicate it on every row. `bIncludeDefault` is read from the caller at `MifBridgeDetails.cpp:748` and a literal `false` is passed at `:778`; the other two call sites (`:830`, `:854`) pass the caller's value. The defect was the SILENCE, not the suppression, and the explanation already existed but was emitted only when `Rows.Num() == 0` - so a caller who matched properties AND asked for defaults was the one guaranteed never to see it.","source":"live, 5.3 probe port 8843, 2026-09-10: `{class:\"StaticMeshActor\", includeDefault:true}` and `...:false` returned byte-identical JSON apart from `elapsedMs`. Now emits `includeDefaultWarning` whenever the class form is given the key at all (`In->HasField`). Fixed in commit a52706a.","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts"},{"id":"A25","fact":"**`set_function_flags` `callInEditor` is written to the function ENTRY NODE's metadata, not to the function's flags, so neither its own response nor `list_functions` can show it.** It sets `Entry->MetaData.bCallInEditor` (`MifBridgeNodes2.cpp:1402-1409`); the response's `flags` object is `SerializeFunctionFlags` (`:1105-1134`), which reads `FUNC_*` bits only. The one read route is the generic property reader on the entry node. Same shape for `add_switch_string` `caseSensitive`: written to `bIsCaseSensitive` (`MifBridgeNodes3.cpp:771`), absent from the response, readable on the node's `objectPath`. **[Reviewed 2026-09-21 against the code - STALE in part: both responses report the flags (MifBridgeNodes2.cpp:1494-1501; MifBridgeNodes3.cpp:779-782); set_function_flags reports callInEditor, add_switch_string reports caseSensitive; list_functions still returns only name and graphId (MifBridgeNodes2.cpp:1494-1502; MifBridgeNodes3.cpp:779-782; MifBridgeIntrospect.cpp:806-818)]**","source":"live, 5.3 probe port 8843, 2026-09-10, scratch `/Game/_MifParamProbe/BP_*` deleted after: `create_function{name:\"FnCIE\"}` -> `set_function_flags{function:\"FnCIE\", callInEditor:true}` answered ok with `callInEditor` nowhere in the response and nowhere in `list_functions`; `get_property{objectPath:\".../BP:FnCIE.K2Node_FunctionEntry_0\", property:\"MetaData.bCallInEditor\"}` read `True`. `add_switch_string{caseSensitive:true}` then `get_property{property:\"bIsCaseSensitive\"}` read `True`, with no case field in the add response.","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts","status":"superseded"},{"id":"A27","fact":"**Every mesh in `/Engine/BasicShapes` is 100 units across: Cube, Cylinder and Sphere all report `get_actor_bounds` extent 50,50,50 at scale 1.** So a scale of *s* is a HALF-extent of 50*s, and a Sphere scaled 2.6 has a radius of 130, not 260. A showcase that treats a scale as a full width places every prop at half the clearance it thinks it has.","source":"live, 5.7 showcase editor port 8860, 2026-09-14: three StaticMeshActors spawned at scale 1 with `spawn_actor_in_level`, measured with `get_actor_bounds`, all three extent `{x:50,y:50,z:50}`; deleted, nothing saved. Used by `tools/make_ue_city.py` to derive `CROWN_R`, `PROP_LINE` and `BUILD_LINE`","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts"},{"id":"A31","fact":"**`maxValueChars` has an undocumented FLOOR of 16 characters, and behaves three different ways across the two property readers.** On `describe_property`'s SURVEY form it clips to exactly N with a floor of 16 and marks the row `valueClipped: true`. On its SINGLE-PROPERTY form (`propertyPath`) it is accepted and IGNORED at every value, with no `valueClipped` either. On `diff_properties_vs_default` it clips to the same floor and marks NOTHING, so a 16-character value there cannot be told from a complete one. **RE-CHECKED 2026-09-20 on live 5.8.2: the floor is still 16 and is no longer undocumented.** `describe_endpoint{endpoint:\"describe_property\"}` now returns an `acceptedSummary` reading `\"... limit, maxValueChars (floor 16), includeMetadata, includeDefault\"`, so the constraint this row was written to record has since been surfaced in the describe table itself. The word \"undocumented\" in the headline is the stale part; the number is not. **[Reviewed 2026-09-21 against the code - STALE in part: single-property describe_property clips when maxValueChars is passed; diff rows flag valueClipped (MifBridgeDetails.cpp:894-916, 629-660, 783, 992)]**","source":"5.7","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts","value":"live, 5.7 probe port 8848, 2026-09-14, on a StaticMeshComponent's RelativeLocation (39 chars raw). Survey form: 4 -> 16, 8 -> 16, 16 -> 16, 20 -> 20, 32 -> 32, 39 and above -> 39 whole. Single-property form: 4, 20 and 1000 all return the full 39 with `valueClipped` absent. Diff endpoint at maxValueChars 12: rows come back 16 long with `valueClipped` None. Re-runnable: `tools/test_ue_property_readers.py` rows T4506, T4507, T4508 and T4513.","status":"superseded"},{"id":"G1","fact":"**A staircase has discrete landing plateaus. A constant linear rise with no plateau is a ramp or a brace.**","source":"the helideck brace rose exactly 30.35 cm per 50 cm of y with zero flat; the real stair's beads sit at four discrete heights 250 cm apart. Cost: 24 lights placed on a structural member. `generator/_rises.py` answers this directly.","engine":"","ue":[],"bl":[],"section":"Geometry heuristics"},{"id":"Mfd6869","fact":"ok:false does NOT mean nothing changed - atomicity is a property of the individual handler's write order, there is no blanket rollback and none is planned; 5 handlers were reordered to validate before creating and 4 more explicitly name what they leave behind because a reorder was not safe.","source":"docs/00_ARCHITECTURE.md:340-346 (both lists verbatim, with 'there is no blanket rollback, and no plan to add one'); docs/02_GOTCHAS.md §10:1175-1190; PM-007 in docs/01_POSTMORTEMS.md:1300","engine":"","ue":[],"bl":[],"section":"MifBridge API contract","value":"`reordered: override_inherited_component, add_component, add_foliage_instances, add_timeline, create_material_instance; self-reporting: add_pin, recipe_add_debug_print, create_struct, set_variable_flags`"},{"id":"M7de191","fact":"Do not name a benign parameter 'force': the audit harness strips force, confirm, save and overwrite from every payload, so auto_map_retarget_chains' harmless force:true always arrived as false, the endpoint did half the work and reported success. It is now remapExisting.","source":"docs/02_GOTCHAS.md §13 § And do not name a benign parameter force (lines 1701-1709)","engine":"","ue":[],"bl":[],"section":"MifBridge API design"},{"id":"Mbf8961","fact":"When an endpoint takes an enum by name it must accept the name the user can SEE: TC_EditorIcon is the authored name and 'UserInterface2D (RGBA)' is the Details-panel name, and set_texture_settings recommended UserInterface2D in four help strings while its own parser rejected it. MifImportParseEnum now falls back to display names on a second pass, normalised (spaces dropped, trailing parenthetical cut), so UserInterface2D, User Interface 2D and TC_EditorIcon all land on the same value.","source":"Engine 5.3.2 source  Runtime/Engine/Classes/Engine/TextureDefines.h:353 - verified verbatim at that exact line. docs/02_GOTCHAS.md §13 § An enum's AUTHORED name; docs/01_POSTMORTEMS.md 2026-09-09 § A UI string is not an API. The fix covers every enum field that parser handles (lodGroup, mipGenSettings, filter), not just com","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"MifBridge API design","value":"`TC_EditorIcon UMETA(DisplayName = \"UserInterface2D (RGBA)\"),`"},{"id":"M816050","fact":"get_property and list_object_properties demand objectPath/propertyPath (not object/property) and get_datatable_row demands rowName; all three self-document via their error strings, a wrong row name returns a clean \"row '<x>' not found in <Table>\", and describe_class returns ALL inherited members so callers must diff against the parent themselves.","source":"Live probes 2026-07-26 recorded in MifBridge docs/audit/work/LIVE_PROBES.md:193; the row-not-found string re-verified in current source at Source/MifBridge/Private/MifBridgeDataTables.cpp:463","engine":"","ue":[],"bl":[],"section":"MifBridge endpoint contracts","value":"`row '%s' not found in %s`"},{"id":"Mff2650","fact":"get_datatable_row is O(whole table) on the found path — it serialises the entire table via GetTableAsJSON then linear-scans for the row name — so an agent looping rows on a large table pays quadratic cost. (A missing row now short-circuits on GetRowMap().Contains.)","source":"MifBridge Source/MifBridge/Private/MifBridgeDataTables.cpp — handler H_get_datatable_row at :433, row-map pre-check :461, GetTableAsJSON + linear scan :472-490. CORRECTION: the candidate's :135-153 is stale","engine":"","ue":[],"bl":[],"section":"MifBridge endpoint defects","value":"`if (ParseJsonArray(Table->GetTableAsJSON(ExportFlags), Rows)) { for (…) if (RowObj->TryGetStringField(\"Name\", ThisName) && ThisName == RowName) … }`"},{"id":"M48ae7a","fact":"Adding an endpoint requires seven files to stay in sync, and only steps 1-2 are link-checkable: MIF_DECL in MifBridgeHandlers.h, MIF_BIND in MifBridgeCommon.cpp Handlers(), the read-only/self-managed classification, the H_name definition, the MifBridgeDescribe.cpp key list whose Summary must be byte-identical to the AcceptedSummary passed to RejectUnknownParams, the @mcp.tool wrapper in tools/mcp-server/server.py, and the docs.","source":"docs/00_ARCHITECTURE.md § Adding an endpoint (:243-256) and § server.py parity (:383 'UE parity' row of the parity_check table, which states the relation as MIF_BIND == _post literals minus recorded exemptions); tools/parity_check.py, which parses both sides with ast and fails closed on anything it cannot read statical","engine":"","ue":[],"bl":[],"section":"MifBridge endpoint hygiene","value":"`parity check: MIF_BIND(...) literals in MifBridgeCommon.cpp == _post(\"...\") literals in server.py - grep _post(, NOT @mcp.tool(), so bl_* and mif_* tools correctly stay out of both columns`"},{"id":"Mbd8272","fact":"A supplied parameter of the wrong JSON type is a hard error naming the field path, value and expected type - never a silent default. Omitting a field still means 'leave it alone'. Enforced centrally so it covers every numeric and boolean parameter on every endpoint (limit, radius, fov, count, confirm), not just transforms; the response carries ignoredParameters when it trips.","source":"docs/02_GOTCHAS.md §9:1112-1132; enforced in RunEndpoint by ResetParamTypeViolations/ReportParamTypeViolations, MifBridgeCommon.cpp:1563 and :1576/:1612; docs/audit/06_IMPLEMENTED.md Batch L; PM-006","engine":"","ue":[],"bl":[],"section":"MifBridge input validation","value":"`vectors take {x,y,z} or [x,y,z]; rotators also {pitch,yaw,roll}; scale also a bare number (uniform); an unrecognised key inside a vector object is refused; \"12.5\" accepted, \"12abc\" refused`"},{"id":"M9fb8c7","fact":"Single-node endpoints accept nodeGuid | node | guid | nodeId interchangeably; multi-node endpoints keep distinct names on purpose because aliasing would let one node satisfy two roles. FGuid::Parse accepts both the dashed 36-char and undashed 32-char forms on every endpoint, and the bridge always emits EGuidFormats::Digits (undashed).","source":"docs/02_GOTCHAS.md §1 § Node identity (lines 296-318), including the note that an apparent dashed-vs-undashed mismatch was always really a field-name mismatch","engine":"","ue":[],"bl":[],"section":"MifBridge parameter grammar","value":"`connect_pins/reconnect_pin: srcNode,dstNode | splice_into_exec: afterNode,insertNode | recipe_splice_before_parent: parentNode,clusterEntry,clusterExit | recipe_argmax_over_components: loopBodyNode,scoreNode,indexNode`"},{"id":"M5b01b9","fact":"MifBridge routes are POST /api/<endpoint_name> with an X-Mif-Token header; the token comes from env MIF_BRIDGE_TOKEN and falls back to \"dev\".","source":"MifBridge Source/MifBridge/Private/MifBridgeServer.cpp:205 (path), :213 (VERB_POST bind), :363 (X-Mif-Token header read); token default Source/MifBridge/Private/MifBridge.cpp:58-61. CORRECTION: the candidate's cited :91/:99 and MifBridge.cpp:33-36 are stale line numbers","engine":"","ue":[],"bl":[],"section":"MifBridge protocol","value":"`const FString PathStr = FString::Printf(TEXT(\"/api/%s\"), *Name); … BindRoute(FHttpPath(PathStr), EHttpServerRequestVerbs::VERB_POST, Handler)`"},{"id":"Mb872ae","fact":"DisableRestorePromptAndDeclinePackageRecovery, the -AutoDeclinePackageRecovery switch and the FApp::IsUnattended() gate are present and identical on UE 5.3, 5.4, 5.5, 5.6, 5.7 and 5.8, so no engine guard is needed around them.","source":"Read directly from all six installs during this verification: the gate line `if(HasPackagesToRestore() && !bAutoDeclineRecovery && !FApp::IsUnattended())` is byte-identical in PackageAutoSaver.cpp on every one (only the line number moves: 436/439/439/670/670/683), each carries exactly one AutoDeclinePackageRecovery par","engine":"","ue":[],"bl":[],"section":"MifBridge recovery modal"},{"id":"Meabe92","fact":"A verification must not share an input with the thing it verifies - if 'wanted' and 'after' both come from the same parse the comparison is a tautology dressed as a check. The generalised form: ask what the check would still catch if the parser were wrong; if the answer is 'nothing', it is the wrong check. The corollary is to validate at the boundary, not after the effect.","source":"docs/01_POSTMORTEMS.md PM-006 § Prevention, items 1 and 2 (:1470-1476). The same document adds the third: every 'lenient parser' in UE (ImportText*, Atod, Atoi, LexTryParseString, FJsonValueString::TryGetNumber) is a silent-default generator - 'the parser returned true' means 'it consumed something', never 'it consumed","engine":"","ue":[],"bl":[],"section":"MifBridge testing method"},{"id":"Mc70484","fact":"`FString`'s comparison operators are CASE-INSENSITIVE. `==`, `!=`, `<`, `<=`, `>` and `>=` all route through `FPlatformString::Stricmp`, so `Origin != TEXT(\"container\")` is false for `\"Container\"`. This matters wherever a handler validates a request string against literals: the guard accepts every casing, and a refusal message claiming the match is case-sensitive is wrong. Use `FString::Equals(Other, ESearchCase::CaseSensitive)` when case must actually count.","source":"Read at Engine/Source/Runtime/Core/Public/Containers/UnrealString.h:739-835 in UE_5.3 (the Stricmp-based operator family). Confirmed live on 5.3 the same day against find_assets' new origin guard: `origin:\"Container\"` answered ok:true while `origin:\"cooked\"` was refused - which is only possible if the comparison ignores case. An earlier draft of that refusal message asserted the opposite; sending both spellings is what caught it, not reading the code. Re-runnable.","engine":"5.3","ue":["5.3"],"bl":[],"section":"UE C++ / FString","value":"`FString == FString  ->  FPlatformString::Stricmp(*Lhs, *Rhs) == 0`"},{"id":"U56","fact":"**`destroy_actor` is in the MifBridge source and is NOT a registered endpoint; the real one is `delete_level_actor` (`actorPath`, `confirm:true`).** Second instance of U49's rule, so it is a pattern rather than a one-off: grep the Source tree for an endpoint name and you will find strings that the live registry does not serve. `self_audit` returns the real list of 458.","source":"measured 2026-09-18: `describe_endpoint {endpoint:\"destroy_actor\"}` -> `status:\"no_such_endpoint\", registered:false`; filtering `self_audit`'s endpoint list for actor create/delete gave `add_spawn_actor, delete_level_actor, remove_actor_from_data_layer, spawn_actor_in_level, spawn_actor_in_pie, spawn_metahuman_actor`.","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE materials, actor identity and PIE"},{"id":"U104","fact":"**CONFIRMS THE FIX U99 PREDICTED.** U99 measured that MifBridge force-enabling GameFeatures and Water put two `LoadErrors: Error` entries into a clean project open, that requiring the dependencies could not help, that plugin-shipped `Config/BaseEngine.ini` did not reach either check, and that the only remaining fix was to stop linking those two plugins. Doing so gives **zero** - the same count as a project with no MifBridge installed at all - and costs no endpoints, because both handler files already carried a complete `#if !MIF_WITH_*` arm and the route count stays at 458.","source":"measured 2026-09-19 on stock UE 5.3. `MIF_WITH_WATER=0` and `MIF_WITH_GAMEFEATURES=0` are now defined without adding the dependency, and both plugins were dropped from the descriptor. The shipped zip was then EXTRACTED and rebuilt through `RunUAT BuildPlugin` against stock 5.3 (BUILD SUCCESSFUL, 0 compile or link errors), and that package opened in a clean Blueprint-only project: `LogMifBridge: Bound 458 routes`, `MifBridge module loaded`, **0** lines matching `failed to load because module`, and **0** `LoadErrors: Error` against **0** in the no-MifBridge control. The import table fell from 67 DLLs to 65 with `UnrealEditor-Water.dll` and `UnrealEditor-GameFeatures.dll` gone, and `tools/audit_plugin_imports.py` returns PASS. Re-runnable: the audit tool, plus `RunUAT BuildPlugin` on the published zip.","engine":"5.3","ue":["5.3"],"bl":[],"section":"UE materials, actor identity and PIE"},{"id":"U408","fact":"**`FPackagePath::FromPackageNameChecked` does not stop on a name that is not a long package name: it logs an Error and returns an empty path.** It calls `TryFromPackageName`, and when that fails it only logs \"FromPackageNameChecked: Invalid LongPackageName\" at Error and returns the default-constructed `FPackagePath` - no check(), no assert. A caller that means to validate a name has to call `TryFromPackageName` or `FPackageName::IsValidLongPackageName` itself. Read, not run.","source":"5.3.2 `Runtime/CoreUObject/Private/Misc/PackagePath.cpp:214-223`, FPackagePath::FromPackageNameChecked; 5.8 `Runtime/CoreUObject/Private/Misc/PackagePath.cpp:215-224`, FPackagePath::FromPackageNameChecked","engine":"5.3.2-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE object allocation and new packages - a name already taken, and a name with `//`"},{"id":"U550","fact":"**UE 5.8's experimental Unreal MCP plugin (ModelContextProtocol, EnabledByDefault false) serves MCP over HTTP at http://127.0.0.1:8000/mcp by default but does not start unless `bAutoStartServer` is set.** Settings live in config=EditorPerProjectUserSettings, section [/Script/ModelContextProtocolEngine.ModelContextProtocolSettings]: ServerUrlPath (default \"/mcp\"), ServerPortNumber (8000), bAutoStartServer (false), bEnableToolSearch (true: tools/list returns only list_toolsets / describe_toolset / call_tool, and toolset tools are reached through call_tool). Toolsets register through the ToolsetRegistry plugin it depends on.","source":"5.8: Plugins/Experimental/ModelContextProtocol/Source/ModelContextProtocolEngine/Public/ModelContextProtocolSettings.h:17-46; 5.8: Plugins/Experimental/ModelContextProtocol/Source/ModelContextProtocol/Public/ModelContextProtocol.h:45-46 (DefaultServerPort = 8000, DefaultServerUrlPath \"/mcp\"); ModelContextProtocol.uplugin (EnabledByDefault false, depends on ToolsetRegistry). Live: with bAutoStartServer=True the server listened on 8000, and initialize plus tools/call list_toolsets answered (2026-10-15).","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U563","fact":"**MifBridge `run_python` (app unreal) runs every call in one persistent Python namespace: a global assigned in one call is still defined in the next. A script that reads optional inputs with `globals().get(\"NAME\", default)` therefore silently reuses the PREVIOUS call's value when the new call does not set NAME.** Pass every input explicitly on each call, read optional inputs as `globals().get(\"NAME\") or default`, and `globals().pop(...)` them after use.","source":"measured 2026-10-16 on 5.8.3: a call set SK_PATH / PREFIX for one asset; a later call that set neither exec'd the same script and it acted on the first asset again (its report listed the first asset's slots); after popping the names a third call read the defaults","engine":"MifBridge (measured on 5.8.3 only)","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"}]