"""Behaviorally-verified facts about the DaVinci Resolve scripting API.

The Resolve scripting API is under-documented and frequently behaves differently
from its apparent signature: methods live on objects you wouldn't expect, return
values lie, string keys are silently rejected, and some documented-looking calls
don't exist. This module records facts we have *verified against live Resolve* so
agents and code can look the reality up instead of rediscovering it the hard way.

Each entry is a small dict. Grow it opportunistically — every readback recipe and
every fix that uncovers a surprising behavior should add an entry. Facts are
stamped with the Resolve build they were verified on so drift is visible when a
new version ships.

Optional keys on an entry feed the Blackmagic-facing limitations report
(``docs/reference/api-limitations.md``, generated by
``scripts/gen_api_limitations.py``):

  - ``submit``: ``"missing"`` (a capability Blackmagic should add) or ``"bug"``
    (a behavior Blackmagic should fix). Entries without ``submit`` are internal
    quirks/tips and stay out of the report.
  - ``issue``: GitHub issue number (int) that drove the finding, for cross-ref.

When you add or change a ``submit``-tagged entry, regenerate the report
(``venv/bin/python scripts/gen_api_limitations.py``) or the
``tests.test_api_limitations_doc`` drift guard fails.
"""
from typing import Any, Dict, List, Optional

VERIFIED_ON = "DaVinci Resolve Studio 21.0.0"

# Each entry: symbol, object, reality, recommended, tags. `signature` optional.
API_TRUTH: List[Dict[str, Any]] = [
    {
        "symbol": "MediaPool.AutoSyncAudio",
        "object": "MediaPool",
        "signature": "(clips, settings) -> bool",
        "reality": "The boolean return does not reflect whether clips actually "
                   "linked, and string enum keys in `settings` are silently "
                   "rejected (the call returns False).",
        "recommended": "Resolve the AUDIO_SYNC_* enum constants via the live "
                       "resolve handle, and verify by reading each clip's "
                       "'Synced Audio' property (see verify_by_readback).",
        "tags": ["unreliable-return", "silent-failure", "audio", "enum"],
        "submit": "bug",
        "mitigation": ["_normalize_auto_sync_settings", "_safe_auto_sync_audio"],
    },
    {
        "symbol": "Timeline.CreateSubtitlesFromAudio",
        "object": "Timeline",
        "signature": "(autoCaptionSettings) -> bool",
        "reality": "Same failure mode as AutoSyncAudio: the autoCaptionSettings "
                   "dict is keyed by resolve.SUBTITLE_* enum constants with "
                   "resolve.AUTO_CAPTION_* enum values, so plain string keys like "
                   "{'language': 'korean'} are silently rejected (returns False, "
                   "no subtitle track created). The boolean is also unreliable.",
        "recommended": "Resolve the SUBTITLE_*/AUTO_CAPTION_* constants via the "
                       "live resolve handle (server._normalize_auto_caption_settings) "
                       "and verify by reading the timeline's subtitle track count "
                       "before/after (server._safe_create_subtitles).",
        "tags": ["unreliable-return", "silent-failure", "subtitle", "enum"],
        "submit": "bug",
        "mitigation": ["_normalize_auto_caption_settings", "_safe_create_subtitles"],
    },
    {
        "symbol": "ProjectManager CloudProject family (Create/Load/Import/RestoreCloudProject)",
        "object": "ProjectManager",
        "signature": "(..., cloudSettings) -> Project | bool",
        "reality": "All four take an enum-keyed {cloudSettings} dict "
                   "(resolve.CLOUD_SETTING_* keys, resolve.CLOUD_SYNC_* sync-mode "
                   "values). Plain string keys are silently rejected, so a settings "
                   "dict built from human-readable keys yields no project / False.",
        "recommended": "Resolve the CLOUD_SETTING_*/CLOUD_SYNC_* constants via the "
                       "live resolve handle (server._normalize_cloud_settings) "
                       "before calling, and treat the bool return from "
                       "Import/RestoreCloudProject as advisory.",
        "tags": ["silent-failure", "project", "cloud", "enum"],
        "submit": "bug",
        "mitigation": ["_normalize_cloud_settings"],
    },
    {
        "symbol": "Timeline.Export",
        "object": "Timeline",
        "signature": "(fileName, exportType, exportSubtype) -> bool",
        "reality": "exportType/exportSubtype must be resolve.EXPORT_* enum *values* "
                   "resolved from the live handle. A JSON/MCP caller cannot pass a "
                   "live enum, and a plain string ('fcpxml', or even the constant "
                   "name 'EXPORT_FCPXML_1_10') is silently rejected with no file "
                   "written.",
        "recommended": "Map a friendly format/subtype to the EXPORT_* constant and "
                       "resolve it against the live handle "
                       "(server._timeline_export_spec) before calling; verify the "
                       "output file exists afterward.",
        "tags": ["silent-failure", "timeline", "export", "enum"],
        "submit": "bug",
        "mitigation": ["_timeline_export_spec", "_timeline_export_value"],
    },
    {
        "symbol": "ProjectManager.DeleteProject",
        "object": "ProjectManager",
        "signature": "(projectName) -> bool",
        "reality": "Returns False (no deletion) when the target project is, or "
                   "recently was, the current project, and is flaky on the first "
                   "attempt — so a single bool() call leaves the project undeleted "
                   "with no useful error.",
        "recommended": "Load/close away from the target first, then retry; use "
                       "src/utils/project_cleanup.py:delete_project_safely.",
        "tags": ["unreliable-return", "project", "flaky"],
        "submit": "bug",
    },
    {
        "symbol": "Composition.Paste",
        "object": "Fusion Composition",
        "reality": "Passing tool.SaveSettings()'s in-memory table to Paste() / "
                   "LoadSettings() fails across the Python bridge with an "
                   "OrderedDict/null-argument error and creates no node, while "
                   "reporting nothing useful.",
        "recommended": "Duplicate via AddTool(RegID) + SaveSettings(path)/"
                       "LoadSettings(path) through a temp .setting FILE, which "
                       "round-trips reliably. Identify the new node by name diff.",
        "tags": ["fusion", "bridge", "silent-failure"],
        "submit": "bug",
    },
    {
        "symbol": "FlowView.SetPos / FlowView.GetPosTable",
        "object": "Fusion FlowView (comp.CurrentFrame.FlowView)",
        "reality": "Node positions are read/written through the FlowView, not the "
                   "tool. SetPos returns nothing reliable; GetPosTable returns a "
                   "1-indexed table (or dict/tuple depending on bridge).",
        "recommended": "Use comp.CurrentFrame.FlowView.SetPos(tool, x, y); confirm "
                       "with GetPosTable and a liberal position parser.",
        "tags": ["fusion", "unreliable-return"],
        "submit": "bug",
    },
    {
        "symbol": "Timeline.GetTimelineByName",
        "object": "Project",
        "reality": "Does not exist. Timelines are looked up by index.",
        "recommended": "Iterate GetTimelineByIndex(1..GetTimelineCount()).",
        "tags": ["missing-method", "timeline"],
        "submit": "missing",
    },
    {
        "symbol": "Project render methods (AddRenderJob, SetRenderSettings, ...)",
        "object": "Project",
        "reality": "Render methods live on the Project object, not on a separate "
                   "render-settings interface.",
        "recommended": "Call proj.AddRenderJob(), proj.SetRenderSettings(), "
                       "proj.LoadRenderPreset() directly on the project.",
        "tags": ["render"],
    },
    {
        "symbol": "MediaPoolItem.GetClipProperty('Transcription')",
        "object": "MediaPoolItem",
        "reality": "Returns a PREVIEW of the transcription that ends in an "
                   "ellipsis when the full transcript is longer than the property "
                   "exposes.",
        "recommended": "Treat a trailing ellipsis as truncation (see "
                       "media_pool_item get_transcription's `truncated` flag).",
        "tags": ["transcription", "truncation"],
        "submit": "bug",
    },
    {
        "symbol": "ProjectManager.CreateProject (with a dirty Untitled project)",
        "object": "ProjectManager",
        "reality": "Returns None and pops a modal 'Save Current Project' dialog "
                   "when the current unsaved/Untitled project blocks the switch. "
                   "SaveProject() on an Untitled project re-triggers the same modal.",
        "recommended": "CloseProject(current) to discard the untitled project "
                       "without a prompt, then CreateProject; restore with "
                       "LoadProject afterward.",
        "tags": ["project", "modal", "silent-failure"],
        "submit": "bug",
    },
    {
        "symbol": "Timeline.InsertFusionCompositionIntoTimeline",
        "object": "Timeline",
        "reality": "Reliable way to obtain a Fusion comp on an otherwise empty "
                   "timeline: it inserts a Fusion composition clip whose comp is "
                   "then reachable via GetFusionCompByIndex(1).",
        "recommended": "Use it (rather than InsertGeneratorIntoTimeline) when you "
                       "need a comp to operate on.",
        "tags": ["fusion", "timeline"],
    },
    {
        "symbol": "Source Track Selector / destination track for Insert*IntoTimeline",
        "object": "Timeline",
        "reality": "There is no API to read or set the Source/Auto Track Selector "
                   "(the Edit-page patch panel that picks the destination track). "
                   "InsertTitleIntoTimeline, InsertFusionTitleIntoTimeline, "
                   "InsertGeneratorIntoTimeline, InsertFusionGeneratorIntoTimeline, "
                   "InsertOFXGeneratorIntoTimeline and "
                   "InsertFusionCompositionIntoTimeline take no trackIndex and "
                   "always drop the clip on the selector's current target (V1 in "
                   "practice). Locking lower video tracks does NOT redirect the "
                   "insert — verified live on 21.0.0: locking V1 makes the insert "
                   "FAIL rather than land on V2. Titles/generators also can't be "
                   "moved afterward (no MediaPoolItem, so AppendToTimeline clipInfo "
                   "and MoveClips don't apply).",
        "recommended": "Accept the limitation for titles/generators (insert lands "
                       "on V1). For clips that DO have a MediaPoolItem, target a "
                       "track with MediaPool.AppendToTimeline's clipInfo 'trackIndex' "
                       "instead (exposed as media_pool append_to_timeline clip_infos). "
                       "See issue #74.",
        "tags": ["missing-method", "timeline", "title", "generator", "track"],
        "submit": "missing",
        "issue": 74,
    },
    {
        "symbol": "Per-clip audio channel-format conversion (Stereo<->Mono)",
        "object": "MediaPoolItem / TimelineItem",
        "reality": "No scripting method converts an individual clip's audio "
                   "channel format. ConvertTimelineToStereo is timeline-wide, and "
                   "CreateStereoClip builds a 3D *visual* stereoscopic clip, not an "
                   "audio mono->stereo change. The Edit-page 'Clip Attributes > "
                   "Audio' channel mapping is UI-only.",
        "recommended": "Use the supported surface: timeline add_track with audioType "
                       "(create mono/stereo tracks), get_track_sub_type (query "
                       "format), convert_to_stereo (timeline-wide), and "
                       "timeline_item get_source_audio_channel_mapping. Per-clip "
                       "conversion is not possible. See issue #73.",
        "tags": ["missing-method", "audio", "channel"],
        "submit": "missing",
        "issue": 73,
    },
    {
        "symbol": "Native multicam clip creation",
        "object": "MediaPool",
        "reality": "There is no method to create a native multicam clip from a set "
                   "of angles. Angles can be stacked onto tracks programmatically, "
                   "but the multicam-clip conversion is a UI-only step.",
        "recommended": "Prepare a stacked timeline (media_pool setup_multicam_timeline) "
                       "and finish the multicam-clip conversion in the Resolve UI.",
        "tags": ["missing-method", "media-pool", "multicam"],
        "submit": "missing",
    },
    {
        "symbol": "Transition create / copy / clone",
        "object": "Timeline / TimelineItem",
        "reality": "The scripting API exposes no method to add, read, copy, or "
                   "clone an edit transition (cross-dissolve, etc.). Transitions "
                   "applied in the UI are invisible to and unmodifiable by scripts.",
        "recommended": "Apply/duplicate transitions in the Resolve UI; no scripted "
                       "equivalent exists.",
        "tags": ["missing-method", "timeline", "transition"],
        "submit": "missing",
    },
    {
        "symbol": "Cloud project enumeration / export / user management",
        "object": "ProjectManager",
        "reality": "Only CreateCloudProject, LoadCloudProject, ImportCloudProject "
                   "and RestoreCloudProject exist. There is no GetCloudProjectList "
                   "(list available cloud projects), no ExportToCloud, and no "
                   "Add/RemoveUserToCloudProject — so cloud collaboration can't be "
                   "fully automated.",
        "recommended": "Drive cloud project listing, export, and collaborator "
                       "management from the Resolve UI; only create/load/import/"
                       "restore are scriptable.",
        "tags": ["missing-method", "project", "cloud"],
        "submit": "missing",
    },
    {
        "symbol": "TimelineItem trim / move / re-time (no position setters)",
        "object": "TimelineItem",
        "reality": "TimelineItem exposes GetStart, GetEnd, GetDuration, "
                   "GetLeftOffset, GetRightOffset and GetSourceStart/EndFrame, but "
                   "NO matching setters. A clip cannot be trimmed, slipped, slid, "
                   "rolled, moved to another time/track, or have its duration "
                   "changed once it is on the timeline. Verified via dir() on "
                   "Resolve 21.0.0 (getters only).",
        "recommended": "Do edit-point adjustments in the Resolve UI, or rebuild the "
                       "timeline from MediaPool.AppendToTimeline clipInfos with the "
                       "desired startFrame/endFrame/recordFrame.",
        "tags": ["missing-method", "timeline", "edit", "trim"],
        "submit": "missing",
    },
    {
        "symbol": "Razor / blade / split a timeline item",
        "object": "Timeline / TimelineItem",
        "reality": "There is no method to split/cut/blade a clip at a given frame. "
                   "Verified absent on Timeline and TimelineItem (dir(), 21.0.0).",
        "recommended": "Split in the Resolve UI, or construct the cut up-front by "
                       "appending two clipInfos with the desired in/out points.",
        "tags": ["missing-method", "timeline", "edit"],
        "submit": "missing",
    },
    {
        "symbol": "Clip speed / retime ratio and speed ramps",
        "object": "TimelineItem",
        "reality": "SetProperty exposes only retime *quality* (RetimeProcess, "
                   "MotionEstimation) and transform/crop/composite/opacity keys — "
                   "not the speed value itself. There is no way to set a clip to a "
                   "given % speed, reverse it, or author a speed ramp. Verified "
                   "against the documented SetProperty key list AND by live "
                   "mutating attempt on 21.0.0: SetProperty('Speed'|'PlaybackSpeed'"
                   "|'RetimeSpeed'|'ClipSpeed', 50) all return False, while "
                   "SetProperty('RetimeProcess', 1) returns True.",
        "recommended": "Set clip speed/retime in the Resolve UI; no scripted "
                       "equivalent exists.",
        "tags": ["missing-method", "timeline", "retime", "speed"],
        "submit": "missing",
    },
    {
        "symbol": "Color node graph editing and primary grade values",
        "object": "Graph / TimelineItem",
        "reality": "The Graph object exposes node enable/label/count, LUT get/set, "
                   "cache mode, ResetAllGrades, ApplyGradeFromDRX and "
                   "ApplyArriCdlLut; TimelineItem adds SetCDL, CopyGrades and color "
                   "versions. But you cannot add, delete, or connect nodes, and you "
                   "cannot read or write primary grade values (lift/gamma/gain/"
                   "offset/contrast/curves/qualifiers/power windows). Grading is "
                   "limited to CDL, whole-grade DRX/LUT application and copying.",
        "recommended": "Build node trees and dial grades in the Resolve UI or via "
                       "DRX/CDL/LUT import; per-parameter grade control is not "
                       "scriptable.",
        "tags": ["missing-method", "color", "grade", "node"],
        "submit": "missing",
    },
    {
        "symbol": "Fairlight audio levels / pan / EQ / automation / FairlightFX",
        "object": "TimelineItem / Timeline",
        "reality": "There is no API to set clip or track volume, pan, EQ, audio "
                   "automation, or to add/configure FairlightFX. SetProperty covers "
                   "video transform only; the audio surface is read-only "
                   "(GetSourceAudioChannelMapping, GetAudioMapping, voice "
                   "isolation). Verified via dir() + SetProperty docs AND by live "
                   "mutating attempt on 21.0.0: SetProperty('Volume'|'Level'|'Gain'"
                   "|'AudioVolume', 0) all return False (note 'Pan' is the VIDEO "
                   "transform key, not audio pan, so it misleadingly succeeds).",
        "recommended": "Mix in the Fairlight UI; only voice-isolation state and "
                       "channel-mapping reads are scriptable.",
        "tags": ["missing-method", "audio", "fairlight"],
        "submit": "missing",
    },
    {
        "symbol": "Proxy / optimized-media generation",
        "object": "MediaPoolItem",
        "reality": "Only LinkProxyMedia, UnlinkProxyMedia and "
                   "LinkFullResolutionMedia exist (attach/detach EXISTING proxies). "
                   "There is no method to generate proxies or optimized media. "
                   "Verified via MediaPoolItem dir() (21.0.0).",
        "recommended": "Trigger proxy/optimized-media generation from the Resolve "
                       "UI; scripting can only link/unlink already-rendered proxies.",
        "tags": ["missing-method", "media-pool", "proxy"],
        "submit": "missing",
    },
    {
        "symbol": "Insert / Overwrite / Replace / Fit-to-Fill edit modes",
        "object": "MediaPool / Timeline",
        "reality": "MediaPool.AppendToTimeline (with optional recordFrame "
                   "positioning) is the only programmatic placement. The standard "
                   "edit modes — insert (ripple), overwrite, replace, fit-to-fill, "
                   "place-on-top — have no API. Verified via dir() (21.0.0).",
        "recommended": "Position clips with AppendToTimeline clipInfo recordFrame, "
                       "or perform insert/overwrite/replace edits in the Resolve UI.",
        "tags": ["missing-method", "timeline", "edit"],
        "submit": "missing",
    },
    {
        "symbol": "Smart Bins / Power Bins creation",
        "object": "MediaPool",
        "reality": "Only AddSubFolder (a regular bin) exists. Smart Bins (rule-"
                   "based) and Power Bins (cross-project) cannot be created or "
                   "configured. Verified via MediaPool dir() (21.0.0).",
        "recommended": "Create Smart/Power Bins in the Resolve UI; only regular "
                       "bins are scriptable.",
        "tags": ["missing-method", "media-pool", "bins"],
        "submit": "missing",
    },
    {
        "symbol": "Per-subtitle text content and timing editing",
        "object": "TimelineItem (subtitle track)",
        "reality": "TimelineItem on a subtitle track exposes only 21 standard "
                   "transform/composite properties (Pan, Tilt, ZoomX, Opacity, "
                   "Crop, etc.). There are no methods to get or set subtitle "
                   "text (GetText/SetText), start time, end time, or duration "
                   "for individual subtitle items. Subtitles created via "
                   "CreateSubtitlesFromAudio or imported via the Resolve UI "
                   "cannot have their content or timing read or modified "
                   "programmatically. Verified via dir() and GetProperty() on "
                   "Resolve 21.0.0.48.",
        "recommended": "No workaround exists — subtitle text and timing are "
                       "completely inaccessible from the scripting API. Must "
                       "be edited in the Resolve UI.",
        "tags": ["missing-method", "subtitle", "text", "timing"],
        "submit": "missing",
    },
    {
        "symbol": "Subtitle track styling and presets",
        "object": "TimelineItem / Timeline / Project",
        "reality": "There is no API method to set or query subtitle font "
                   "family, font size, text color, background color, outline, "
                   "shadow, position, alignment, or to apply/query subtitle "
                   "style presets. TimelineItem.GetProperty() on subtitle "
                   "items returns only transform/composite keys. "
                   "Timeline.GetSetting() and Project.GetSetting() return "
                   "None for all probed subtitle-style keys (e.g. "
                   "'subtitleFontName', 'subtitleFontSize', "
                   "'subtitleTextColor', 'subtitleBackgroundColor', "
                   "'subtitlePosition', 'subtitleAlignment', "
                   "'subtitlePreset', 'subtitleStyle'). Verified via dir(), "
                   "GetProperty(), and GetSetting() on Resolve 21.0.0.48.",
        "recommended": "No workaround exists — subtitle styling is UI-only. "
                       "Burn-in overlays via Fusion titles are a visual "
                       "alternative but do not produce proper subtitle tracks.",
        "tags": ["missing-method", "subtitle", "style", "preset"],
        "submit": "missing",
    },
    {
        "symbol": "Speech recognition engine selection and SRT import",
        "object": "Timeline",
        "reality": "Timeline.CreateSubtitlesFromAudio(autoCaptionSettings) "
                   "always uses the built-in Resolve speech recognition "
                   "engine. There is no API parameter to select an alternative "
                   "provider (e.g. whisper-cli, Google Speech, AWS Transcribe). "
                   "The language selection via resolve.AUTO_CAPTION_LANGUAGE_* "
                   "is the only customization; the engine itself cannot be "
                   "changed. Furthermore, there is no API method to import an "
                   "SRT file into a subtitle track programmatically — "
                   "File -> Import -> Subtitle is UI-only.",
        "recommended": "No workaround exists for provider selection or SRT "
                       "import. External transcripts must be converted to SRT "
                       "and imported through the Resolve UI.",
        "tags": ["missing-method", "subtitle", "transcription",
                 "speech-recognition", "asr"],
        "submit": "missing",
    },
    {
        "symbol": "Media Pool folder rename",
        "object": "MediaPool",
        "reality": "MediaPool exposes AddSubFolder(name), "
                   "DeleteSubFolders([names]), and "
                   "MoveFolders([names], targetFolder) but no "
                   "RenameSubFolder(oldName, newName) method. Folders can be "
                   "created, deleted, and moved, but their names cannot be "
                   "changed through the API. Verified via dir() on Resolve "
                   "21.0.0.",
        "recommended": "Delete and recreate the folder with the desired name, "
                       "or rename in the Resolve UI.",
        "tags": ["missing-method", "media-pool", "folder"],
        "submit": "missing",
    },
    {
        "symbol": "hasattr() / getattr() on Resolve API objects (attribute fabrication)",
        "object": "(all Resolve scripting objects)",
        "reality": "The Python bridge returns a callable for ANY attribute name, so "
                   "hasattr(obj, 'TotallyMadeUpMethod') is always True and getattr "
                   "never raises. This makes capability detection by hasattr "
                   "impossible — verified on 21.0.0 (hasattr reported SetStart, "
                   "Razor, AddNode, GenerateProxy, AddSmartBin etc. as present "
                   "though none exist). Only dir() lists the real methods.",
        "recommended": "Never probe method existence with hasattr/getattr; test "
                       "membership against dir(obj) instead. Calling a fabricated "
                       "method typically returns None/False with no error.",
        "tags": ["bridge", "introspection", "silent-failure"],
        "submit": "bug",
    },
    {
        "symbol": "subprocess inheriting stdin under the MCP stdio server",
        "object": "(server runtime)",
        "reality": "A child process that inherits stdin can race-read bytes off "
                   "the JSON-RPC protocol stream and corrupt it; capture_output "
                   "redirects only stdout/stderr.",
        "recommended": "Pass stdin=subprocess.DEVNULL on every subprocess that can "
                       "run while serving over stdio.",
        "tags": ["runtime", "stdio", "subprocess"],
    },
    {
        "symbol": "MediaPoolItem.SetClipProperty('Reel Name', ...)",
        "object": "MediaPoolItem",
        "signature": "(propertyName, propertyValue) -> bool",
        "reality": "Setting the 'Reel Name' clip property returns True but the "
                   "value is silently dropped on read-back when the project is "
                   "configured to derive reel names automatically "
                   "(General Options > 'Assist using reel names from the:' set "
                   "to source clip file / embedding / filename pattern). The "
                   "same True-but-unpersisted behavior occurs via "
                   "SetMetadata('Reel Name', ...). Other clip properties on the "
                   "same clip (e.g. 'Comments') write and persist normally, so "
                   "this is field-specific, not a bridge/permission failure. "
                   "Verified on Resolve 21.0.0; reported as issue #77.",
        "recommended": "After writing 'Reel Name', read it back with "
                       "GetClipProperty('Reel Name') and refuse to report "
                       "success on mismatch; surface the project-setting gate to "
                       "the caller (server._verify_clip_property_writeback).",
        "tags": ["unreliable-return", "silent-failure", "metadata", "reel-name"],
        "submit": "bug",
        "issue": 77,
        "mitigation": ["_verify_clip_property_writeback", "_verify_writeback"],
    },
]


def lookup_api_truth(query: Optional[str] = None) -> List[Dict[str, Any]]:
    """Return verified facts matching `query`, or all facts if no query.

    Matches a case-insensitive substring against the symbol, tags, and reality.
    """
    if not query:
        return list(API_TRUTH)
    q = query.lower()
    out = []
    for e in API_TRUTH:
        hay = " ".join([
            e.get("symbol", ""),
            e.get("object", ""),
            e.get("reality", ""),
            " ".join(e.get("tags", [])),
        ]).lower()
        if q in hay:
            out.append(e)
    return out


def submittable_limitations() -> Dict[str, List[Dict[str, Any]]]:
    """Group ``submit``-tagged entries for the Blackmagic-facing report.

    Returns ``{"missing": [...], "bug": [...]}`` — entries Blackmagic should add
    vs. behaviors they should fix — preserving API_TRUTH order within each group.
    """
    groups: Dict[str, List[Dict[str, Any]]] = {"missing": [], "bug": []}
    for e in API_TRUTH:
        kind = e.get("submit")
        if kind in groups:
            groups[kind].append(e)
    return groups
