{
  "format": "scripttap-ai-json-contract",
  "version": 1,
  "bridgeScriptUpload": {
    "method": "POST",
    "path": "/scripts/transaction",
    "healthDiscoveryField": "scriptUpload",
    "topLevelFields": [
      "scripts",
      "deleteScriptIds",
      "folders"
    ],
    "requiredTopLevelFields": [
      "scripts"
    ],
    "scriptMemberFields": [
      "clientId",
      "addressedScriptId",
      "script"
    ],
    "requiredScriptMemberFields": [
      "clientId",
      "script"
    ],
    "createRule": "Omit addressedScriptId. ScriptTap assigns the final live ID and returns it in scriptIdMap.",
    "replaceRule": "Set addressedScriptId to the exact live ID returned by GET /scripts.",
    "identityRule": "scriptId is not a transaction-member field. Omit script.id unless it exactly equals addressedScriptId.",
    "scriptBodyRule": "Every member contains one complete public script body. name and commands are required; folderId and disabled are optional.",
    "callRule": "targetMacroId may use another transaction member clientId for remapping or an exact installed live script ID.",
    "atomicity": "All supplied members are accepted into canonical RAM atomically, then one immutable operation enters background persistence.",
    "largePayloadRule": "Assemble all scripts[] members into one local UTF-8 transaction file and send the file as the single request body; never inline large JSON in PowerShell.",
    "requestRule": "Each call is one independent transaction with one terminal response. Separate calls are never retried, joined, or coalesced.",
    "acceptedResponse": {
      "accepted": true,
      "message": "import success",
      "scriptIdMap": {
        "package-local-client-id": "final-live-script-id"
      }
    },
    "replacementExample": {
      "scripts": [
        {
          "clientId": "solver-update",
          "addressedScriptId": "LIVE_ID_FROM_GET_SCRIPTS",
          "script": {
            "name": "Solver",
            "folderId": "",
            "disabled": false,
            "commands": []
          }
        }
      ]
    }
  },
  "scriptPackage": {
    "requiredLiteralFields": {
      "format": "scripttap-json-script",
      "version": 1,
      "kind": "script_package"
    },
    "topLevelFields": [
      "format",
      "version",
      "kind",
      "packageId",
      "createdAtMs",
      "source",
      "rootScriptId",
      "requirements",
      "folders",
      "scripts",
      "assetRequirements",
      "notes"
    ],
    "allowedSourceValues": [
      "user_export",
      "ai_generated",
      "script_builder"
    ],
    "requirementsFields": [
      "commandCatalogVersion",
      "requiresUserCompletion",
      "requiresAssets"
    ],
    "folderFields": [
      "id",
      "name",
      "parentId"
    ],
    "scriptFields": [
      "id",
      "name",
      "folderId",
      "disabled",
      "commands"
    ],
    "assetRequirementFields": [
      "scriptId",
      "commandId",
      "commandType",
      "assetName",
      "reason"
    ],
    "allowedAssetRequirementReasons": [
      "user_must_capture",
      "user_must_attach_existing",
      "source_export_requires_recapture",
      "ai_cannot_provide"
    ],
    "idPrefixes": {
      "packageId": "pkg_",
      "scriptId": "script_",
      "folderId": "folder_",
      "commandId": "cmd_"
    },
    "idAllowedCharacters": "A-Z a-z 0-9 _ -",
    "idSemantics": {
      "scriptPackageIdsArePackageLocal": true,
      "notLiveAppScriptIds": true,
      "importAssignsFreshRealAppIds": true,
      "inPackageScriptReferencesRemapThroughPackageIds": true
    },
    "importValidationSemantics": {
      "importedPackagesDoNotCarryTrustedValidationOutcomeFields": true,
      "readinessIsDerivedLocallyFromAcceptedCommandGraph": true,
      "manualImportSchedulesValidationImmediately": true,
      "bridgeFullScriptQuietPeriodMs": 60000,
      "bridgeQuietAppliesTo": "accepted full-script create, replace, and transaction only",
      "syntaxAndCallTopologyCheckedBeforeRamAcceptance": true,
      "dependencyCompletenessIsNotCheckedDuringImport": true,
      "validPackagesCommitAllOrNothing": true,
      "acceptancePublishesCanonicalRamAndQueuesBackgroundPersistence": true,
      "acceptanceDoesNotWaitForSsd": true,
      "ramAdmissionChecks": [
        "public_schema",
        "syntax",
        "ordinary_paired_block_balance",
        "call_topology",
        "storage_admission"
      ],
      "admissionIsNotValidationV2Completion": true,
      "admissionIsNotRunReadiness": true,
      "babyIfIngressChecks": "Public field names/types plus ordinary IF/ELSE/End IF balance.",
      "aiPackagesShouldOmitBabyIfFields": [
        "babyIfAttached",
        "babyIfParentId",
        "babyIfSource"
      ],
      "babyIfFieldsAreAppGeneratedRelationshipFields": true,
      "babyIfEligibleSources": [
        "img_find_boolean",
        "set_variable_single",
        "get_pixel_color_result"
      ],
      "schemaValidBabyIfFieldsArePreservedRemappedAndStored": true,
      "babyIfRelationshipValidationRunsAfterCommit": true,
      "babyIfRelationshipFacts": "Parent eligibility, parent/source match, ordering, contiguity, and relationship integrity may publish baby_if_* incomplete issues.",
      "babyIfRelationshipIssuesAreNotImportRefusalRules": true,
      "validationPolling": "After acceptance, observe /scripts/{id}/validation.",
      "validationReadsAreObservationalOnly": true,
      "nonterminalStates": "blank, delayed, queued, validating",
      "nonterminalStateRequiresMutationRetry": false,
      "onlyTerminalCompleteClearsValidationBlocking": true,
      "terminalIncompleteRequiresCorrection": true,
      "runtimePermissionsAndPreflightAreSeparate": true
    },
    "nameLimits": {
      "scriptNameMaxChars": 28,
      "folderNameMaxChars": 28,
      "nicknameMaxChars": 20
    },
    "valueRules": {
      "integerLiteralRange": "-2147483648..2147483647",
      "integerLiteralFormat": "signed whole decimal integer only; decimals, blanks, malformed numbers, and overflow values are invalid",
      "variableToken": "Any one ASCII letter plus optional index 1..4096 is a valid variable token. i/b/h/s/t are type conventions, not enforced letter types.",
      "booleanLiteralStorage": "Boolean SetVariable input may be JSON true/false or true/false strings; saved command JSON canonicalizes to True/False.",
      "modVariableResult": "Decimals are rounded. Division by 0 returns 0. Negative Integer results are kept. Results outside signed 32-bit range are limited to -2147483648..2147483647.",
      "validatorIdentity": "Declaration order, one ASCII letter, exact type, and LOCAL/THREAD scope. Numeric suffixes, ranges, capacities, and initialized slots are not validation facts.",
      "writerAndStorage": "One writer declares the whole typed letter family. Array lengths and counts control attempted initialization or writes only. Runtime storage is sparse.",
      "typedZeroReads": "Missing, unwritten, incompatible, invalid, or out-of-domain reads return Integer 0, Decimal 0, Boolean False, Hex 000000, empty String, or Time 00:00:00 as expected.",
      "invalidWriteAndCountPolicy": "Invalid writes are discarded. Calculated counts outside 0 through 4096 become zero. Execution continues; compact reasons stay in Run Logs only without a modal, banner, toast, or extra result record.",
      "scopeWire": "variableShared false means LOCAL and true means THREAD; it is the sole scope wire encoding.",
      "packageMetadataBoundary": "Packages contain no validation status, issue, revision, index, phase, owner state, or other validation outcome metadata."
    }
  },
  "commandBaseFields": [
    "id",
    "type",
    "nickname",
    "disabled"
  ],
  "forbiddenAnywhere": [
    "assets",
    "images",
    "screenshots",
    "files",
    "bytesBase64",
    "archiveBytes",
    "sts",
    "filesystemPath",
    "accountData",
    "bridgeToken",
    "firebaseData",
    "runtimeLogs",
    "routineRecords",
    "routineCloudState",
    "cloudState",
    "cloudRecoveryState",
    "shortcutTokens",
    "publicRoutinePackages"
  ],
  "forbiddenCommandFields": [
    "estimatedWorkPerSecond",
    "workEstimateVersion",
    "validationAssetGeneration",
    "tolerance",
    "smartTapTolerance",
    "visualTargetColorTolerance",
    "visualPointerTolerance"
  ],
  "publicCommandTypes": [
    "click",
    "delay",
    "delay_until",
    "retap",
    "multi_click",
    "swipe",
    "img_find",
    "smart_tap",
    "label",
    "goto",
    "loop",
    "end_loop",
    "break_loop",
    "call",
    "break_call",
    "stop_call",
    "pause_concurrent",
    "unpause_concurrent",
    "break_exclusive",
    "set_variable",
    "mod_variable",
    "text_type",
    "get_pixel_color",
    "pixel_sample_batch",
    "pixel_find",
    "pixel_color_result",
    "visual_trigger",
    "device_open_app",
    "device_brightness",
    "device_volume",
    "device_do_not_disturb",
    "device_rotation",
    "divider",
    "else",
    "or",
    "end_or",
    "xor",
    "end_xor",
    "and",
    "end_and",
    "rgb_confidence",
    "screenshot",
    "prompt_me",
    "color_classify",
    "array_get",
    "array_set",
    "for_each_index",
    "end_for_each_index",
    "array_segment_scan",
    "append_points",
    "device_settings",
    "pinch",
    "ui_find",
    "device_intent_action",
    "device_system_nav",
    "http",
    "clipboard",
    "file",
    "text_transform",
    "ocr",
    "alert",
    "reader",
    "json",
    "translate",
    "display",
    "touch_down",
    "touch_move",
    "end_touch",
    "set_coordinates"
  ],
  "flowPairingRules": [
    "loop pairs with end_loop",
    "pause_concurrent pairs with unpause_concurrent",
    "or pairs with end_or",
    "and pairs with end_and",
    "xor pairs with end_xor",
    "rgb_confidence uses an IF end row and should be kept structurally inside the IF segment",
    "else may appear only inside an IF segment and before that segment's end row"
  ],
  "assetPolicy": {
    "jsonContainsAssetBytes": false,
    "assetReferencesAreNamesOnly": true,
    "missingAssetsRequireAssetRequirements": true,
    "assetBackedCommandsWithMissingAssetsMustBeIncomplete": true
  },
  "commandSpecificRules": {
    "call": {
      "targetScope": "Complete enabled calls must target a script included in the same package.",
      "topology": "Call targets must be package-local, same-folder-tree, and downline-only. A Call attaches a same-folder orphan script root as the caller's child or makes another runnable call to an existing direct child. Do not call a parent/ancestor, sibling, sibling's child, grandchild, other branch, or external script.",
      "maxScriptLayers": 5,
      "depthRule": "The root is layer 1. A Call that would create layer 6 is rejected before mutation.",
      "draftRows": "Blank enabled Call rows are incomplete drafts. Nonblank missing-target or illegal Call topology rows are rejected before storage."
    },
    "set_variable": {
      "setVariableModeValues": [
        "single",
        "array"
      ],
      "variableTypeValues": [
        "integer",
        "boolean",
        "hex",
        "string",
        "time"
      ],
      "variableNames": "Single mode accepts exactly one variableName token. Array mode accepts exactly one variableLetter plus variableArraySize as an attempted initialization count or a prior Integer variable. Any ASCII letter is valid; typed letters are naming conventions.",
      "booleanValue": "Boolean variableValue accepts JSON booleans and true/false strings on input, then stores canonical True/False.",
      "arrayVsArraySet": "SetVariable single and array modes each declare one typed letter family. Array mode attempts variableArraySize initial writes; ArraySet performs later sparse writes under the same declaration-order, type, and scope rules.",
      "setVariableFromClipboard": "setVariableFromClipboard=true is valid only in single String mode. Leave variableValue blank; runtime reads the current runtime clipboard into variableName. If the clipboard is unavailable, empty, or blocked, runtime logs a warning and leaves existing value unchanged."
    },
    "set_coordinates": {
      "setCoordinatesModeValues": [
        "point",
        "area"
      ],
      "pointFields": "Point mode requires signed Integer literals setCoordinatesPointX/setCoordinatesPointY and distinct canonical Integer outputs setCoordinatesPointXVariableName/setCoordinatesPointYVariableName.",
      "areaFields": "Area mode requires signed Integer literals setCoordinatesAreaLeft/setCoordinatesAreaTop/setCoordinatesAreaRight/setCoordinatesAreaBottom and four distinct canonical Integer outputs with matching *VariableName fields.",
      "scope": "variableShared is required as the sole LOCAL/THREAD scope wire for the active output declarations.",
      "inactiveFields": "Inactive-mode coordinate and output fields do not affect completeness.",
      "builderGuidance": "Builder AI must use set_coordinates instead of ordinary set_variable for portable literal screen coordinates. One command assigns a Point to 2 Integer variables or an Area to 4. SetVariable numeric values remain exact and do not transpose.",
      "import": "Manual, Public Cloud, and Personal Cloud imports transpose literal fields once to the destination device frame. Bridge imports remain exact.",
      "runtime": "Pure RAM Integer writes only; no capture, permission, storage, callback, retry, fallback, or screen interaction."
    },
    "get_pixel_color": {
      "coordinates": "Use required pixelXValue and pixelYValue as the public coordinate token fields. Each accepts an Integer literal or a prior Integer variable.",
      "outputs": "variableName is one Hex output variable. Undefined syntactically valid Hex output targets are allowed; existing wrong-type targets are incomplete.",
      "validation": "pixelXValue and pixelYValue must be explicit nonblank token fields. Missing, blank, malformed, or known non-Integer coordinate variables leave the command incomplete before runtime. Explicit literal 0,0 is valid; missing coordinates must not be invented as 0,0.",
      "runtime": "Runtime resolves only pixelXValue/pixelYValue. Missing token fields leave the command invalid and never fall back to retired x/y fields. Logs and descriptions display the token values, for example GetPixelColor i1,i2 -> h1."
    },
    "pixel_color_result": {
      "ownership": "ScriptTap generates this child Result row under GetPixelColor. AI packages should not invent standalone Result rows.",
      "fields": "variableValue stores the captured Hex color and variableName stores the related Hex variable.",
      "convertToTap": "Convert to Tap creates a linked Baby IF/Tap/End IF block using the parent GetPixelColor coordinates and the Result variable/color.",
      "promote": "Promote detaches the linked Baby IF into an ordinary IF block. AI packages should omit babyIfAttached, babyIfParentId, and babyIfSource."
    },
    "pixel_sample_batch": {
      "sourceModes": "pixelSampleSourceMode must be points or arrays. Points mode uses enabled points[] rows with x/y Integer literals or prior Integer variables. Arrays mode reads Integer xArrayLetter/yArrayLetter slots from startIndexValue for lengthValue rows.",
      "outputs": "arrayLetter is one Hex family letter and indexValue is the 1-based attempted output start. Invalid or out-of-domain writes are discarded.",
      "limit": "At most 2048 sample points per command.",
      "runtime": "Explicit direct pixel-sample batching uses one current accepted screen frame through the direct pixel-sample path, never hidden stale frames. Invalid coordinate reads use Integer zero and invalid Hex writes are discarded while execution continues.",
      "aiUsage": "Use PixelSampleBatch instead of repeated GetPixelColor when many known coordinates are sampled from the same screen state. For board/grid scripts, build X/Y Integer arrays once, use arrays mode, and process the returned Hex array. Use GetPixelColor only for one-off single-pixel checks.",
      "arraysModeExample": {
        "type": "pixel_sample_batch",
        "nickname": "Read board cell colors",
        "pixelSampleSourceMode": "arrays",
        "xArrayLetter": "e",
        "yArrayLetter": "f",
        "startIndexValue": "1",
        "lengthValue": "81",
        "arrayLetter": "h",
        "indexValue": "1"
      }
    },
    "pixel_find": {
      "findAll": "Find All writes up to pixelFindMaxResultsValue entries into 1-based output arrays. pixelFindMaxResultsValue accepts a 1..4096 Integer literal or prior Integer variable, defaults to 255, and applies only when findAll=true. First accepted match writes slot 1.",
      "ignoreRadius": "pixelIgnoreRadius accepts a non-negative Integer literal or prior Integer variable, defaults to 64, and suppresses later accepted points within that radius from an already accepted point.",
      "midpoints": "pixelFindMidpoints=true groups connected matching pixels into one rounded midpoint per block before ignore-radius filtering.",
      "zeroAreaGuidance": "Literal area 0,0-0,0 is syntactically valid but normally means the area was never selected. AI should provide the intended area unless the user explicitly wants that one-pixel origin area.",
      "gridRecipe": "For grids with white gutters, use colored cell-interior areas or crops instead of searching the white separators."
    },
    "clipboard": {
      "clipboardModeValues": [
        "get",
        "set"
      ],
      "clipboardTextSourceModeValues": [
        "literal",
        "variable"
      ],
      "get": "Clipboard.get requires clipboardOutputVariableName as one String output. It reads current plain/coercible Android clipboard text.",
      "set": "Clipboard.set writes plain text from clipboardTextValue or a prior String clipboardTextVariableName. Set mode allows blank literal text.",
      "outputs": "Optional outputs use clipboardSaveSuccess/clipboardSuccessVariableName as Boolean and clipboardSaveError/clipboardErrorVariableName as String.",
      "runtime": "Uses public Android ClipboardManager APIs only, handles plain/coercible text only, does not paste, append, watch, poll, trigger, or handle files/images/URIs as rich content. It does not log clipboard contents by default."
    },
    "text_transform": {
      "textTransformOperationValues": [
        "contains",
        "extract",
        "replace",
        "split",
        "join",
        "trim",
        "case",
        "parse_int",
        "find_all"
      ],
      "textTransformSourceModeValues": [
        "literal",
        "variable"
      ],
      "replaceFindModeValues": [
        "literal",
        "regex"
      ],
      "splitModeValues": [
        "literal",
        "regex"
      ],
      "trimModeValues": [
        "both",
        "left",
        "right"
      ],
      "caseModeValues": [
        "lower",
        "upper"
      ],
      "input": "All operations except join read input through textInputSourceMode as literal textInputValue or prior String textInputVariableName.",
      "outputs": "contains writes Boolean textContainsOutputVariableName. extract/replace/join/trim/case write one String output. split writes sparse String-family slots starting at textSplitOutputStartIndexValue and can optionally save Integer textSplitCountVariableName. find_all writes whole matches or regex capture groups to a matching String family starting at textFindAllOutputStartIndexValue and can optionally save Integer textFindAllCountVariableName. parse_int writes Integer textParseIntOutputVariableName. Optional result outputs use textSaveSuccess/textSuccessVariableName and textSaveError/textErrorVariableName.",
      "regex": "extract, replace regex mode, split regex mode, and find_all regex mode use Java Pattern. Bad regex fails safely.",
      "splitJoin": "Split defaults to 255 attempted parts. Calculated counts outside 0 through 4096 become zero; invalid sparse String writes are discarded. Join returns empty String for missing, invalid, incompatible, or out-of-domain source reads.",
      "findAll": "find_all uses textFindAllFindMode literal/regex, textFindAllPatternValue, textFindAllResultMode whole_match or regex-only capture_group, textFindAllCaptureGroupIndexValue for capture groups, matching String-family textFindAllOutputArrayLetter, textFindAllOutputStartIndexValue, and textFindAllMaxMatchesValue. Max 0 attempts no matches; calculated counts outside 0 through 4096 become zero.",
      "runtime": "Pure Java string logic only: no JavaScript, Lua, expression engine, file, network, clipboard, screen, or permission side effects. String outputs share the normal String variable byte cap."
    },
    "json": {
      "jsonModeValues": [
        "extract",
        "exists",
        "length",
        "stringify"
      ],
      "jsonPathSourceModeValues": [
        "literal",
        "variable"
      ],
      "jsonExtractOutputTypeValues": [
        "string",
        "integer",
        "boolean",
        "string_array",
        "json_text"
      ],
      "jsonStringifyFormatValues": [
        "compact",
        "pretty"
      ],
      "input": "JSON reads one prior String variable through jsonInputVariableName. There is no literal/file/network/clipboard/SAF/HTTP/asset/screenshot source.",
      "path": "Path comes from literal jsonPathValue or one prior String jsonPathVariableName. Supported grammar is only $, .key, nested dot keys, [0] non-negative array indexes, and double-quoted bracket keys with clean escaped quotes/backslashes. Do not use wildcard, filter, recursive descent, expression, slice, negative index, JavaScript, Lua, or any expression engine.",
      "outputs": "extract writes according to jsonExtractOutputType: String, Integer, Boolean, String array jsonExtractArrayLetter starting at jsonExtractArrayStartIndexValue with optional Integer count, or JSON text. exists writes Boolean jsonExistsOutputVariableName. length writes Integer jsonLengthOutputVariableName. stringify writes String jsonStringifyOutputVariableName in compact or pretty format. Optional outputs use jsonSaveSuccess/jsonSuccessVariableName as Boolean and jsonSaveError/jsonErrorVariableName as String.",
      "runtime": "Uses a real org.json parser and never parses JSON with regex/string slicing. Bad JSON/path/source variables fail safely. exists writes false and success true when the path is missing; JSON null counts as existing. length supports array count, object key count, and string character count only. extract null writes blank only for String output and null for json_text; Integer, Boolean, and String-family reads use their typed-zero policy. Object/array values require json_text, not plain String. String array extraction attempts sparse writes in order; invalid writes are discarded. stringify produces valid JSON text capped like normal String variables. Run logs do not include full JSON input."
    },
    "translate": {
      "fields": [
        "translateSourceVariableName",
        "translateTargetVariableName",
        "translateSourceLanguageTag",
        "translateTargetLanguageTag"
      ],
      "default": {
        "type": "translate",
        "translateSourceVariableName": "s1",
        "translateTargetVariableName": "s2",
        "translateSourceLanguageTag": "es",
        "translateTargetLanguageTag": "en"
      },
      "input": "Translate reads exactly one prior String variable through translateSourceVariableName and writes exactly one String variable through translateTargetVariableName.",
      "languageTags": "Source and target language tags are BCP-47-style ML Kit tags from TranslateLanguage.getAllLanguages(), normalized through TranslateLanguage.fromLanguageTag(...). Do not invent unsupported tags.",
      "completeness": "Source and target variables cannot use the same variable. Source and target languages cannot match. Source and target language packs must be installed before the command is complete; missing packs keep the command incomplete until the user explicitly downloads them from ScriptTap.",
      "runtime": "Uses Google ML Kit On-Device Translation only. Runtime never downloads models and never uploads translation input/output to Cloud Translation or a website. Incomplete scripts do not play. Unsupported language tags and translation failures are runtime failures for otherwise complete commands.",
      "privacy": "Translation input/output stays on device. Explicit user-triggered language model downloads may contact Google servers to fetch the selected model."
    },
    "file": {
      "fileModeValues": [
        "read",
        "create",
        "append"
      ],
      "fileFormatValues": [
        "text",
        "csv",
        "json"
      ],
      "fileSourceModeValues": [
        "literal",
        "variable"
      ],
      "fileCreateCsvSourceModeValues": [
        "csv_text",
        "array_row"
      ],
      "fileCreateIfExistsModeValues": [
        "overwrite",
        "new_name"
      ],
      "fileCreateNewNameModeValues": [
        "timestamp",
        "number"
      ],
      "csvReadModeValues": [
        "cell",
        "row",
        "column"
      ],
      "safOnly": "File commands require a user-selected persisted content:// SAF file URI for read/append or folder tree URI for create, saved by the ScriptTap editor. Do not provide raw filesystem paths, broad-storage paths, root paths, or runtime file-picker instructions.",
      "textRead": "File.read Text/JSON requires fileReadTextOutputVariableName as one String output. JSON is raw whole-file text only.",
      "csvRead": "File.read CSV supports cell, row, and column reads. Rows/columns are 1-based. Header lookup uses fileCsvColumnHeaderName against the first CSV row. Row/column outputs attempt sparse String-family writes from fileCsvOutputStartIndexValue for fileCsvArrayValueCount offsets; calculated counts outside 0 through 4096 become zero and invalid writes are discarded.",
      "create": "File.create writes Text/CSV/JSON into a persisted SAF folder. Name source is text or a prior String variable. Text/JSON content is exact literal/prior String text. CSV content is exact CSV text or one String array row serialized with CSV escaping. If Exists defaults to new_name; overwrite must be explicit. New Name timestamp appends MMddyyHHmmss before the extension; number picks the smallest missing positive suffix from one sibling enumeration.",
      "textAppend": "File.append Text writes fileAppendTextValue or one prior String fileAppendTextVariableName, with optional fileAppendNewline.",
      "csvAppend": "File.append CSV writes fileAppendCsvRowValue or one prior String fileAppendCsvRowVariableName as exactly one logical row, re-serialized with CSV escaping and one row terminator. fileAppendCsvEnsureNewRow asks runtime to avoid corrupting a non-newline-terminated final row.",
      "jsonAppend": "JSON append is unsupported. Do not generate append mode with fileFormat json.",
      "outputs": "Optional outputs use fileSaveSuccess/fileSuccessVariableName as Boolean, fileSaveError/fileErrorVariableName as String, fileSaveFinalName/fileFinalNameVariableName as String, and fileSaveFinalUri/fileFinalUriVariableName as String.",
      "runtime": "Runtime only uses existing persisted SAF grants. Text/JSON reads cap at 256KB and can still succeed with text_truncated_256kb. CSV reads cap at 1MB and fail if truncated. Operational file failures interrupt with File Error Stop Script/Continue; Number suffix exhaustion shows LevelUp and stops. No runtime picker, raw path access, broad storage, root, ADB dependency, or file-content logging."
    },
    "alert": {
      "alertModeValues": [
        "play",
        "stop"
      ],
      "alertSourceTypeValues": [
        "file",
        "ringtone"
      ],
      "alertPlayModeValues": [
        "once",
        "loop"
      ],
      "alertSoundExecutionValues": [
        "concurrent",
        "wait"
      ],
      "alertVolumeModeValues": [
        "keep",
        "set"
      ],
      "audioFile": "Audio File source requires a user-selected persisted content:// SAF audio URI selected in ScriptTap, with alertFileHasReadAccess=true and a durable read grant. Do not use raw filesystem paths, broad storage, root paths, ADB, or runtime picker steps.",
      "ringtone": "Ringtone source requires a public Android ringtone URI selected through RingtoneManager. Store alertRingtoneUri and optional alertRingtoneLabel only.",
      "play": "Alert.play requires alertSourceType and the matching selected URI. alertPlayMode once plays once; loop repeats until alertLoopTimeoutMsValue, Alert.stop, runtime abort, permission pause, service exit, or app exit. Loop timeout must be positive, defaults to 30000 ms when blank/invalid/zero, and is capped at 180000 ms.",
      "stop": "Alert.stop stops only ScriptTap-owned Alert playback for the active runtime and restores any ScriptTap-set media volume.",
      "sound": "alertSoundExecution concurrent returns immediately after playback starts. wait blocks until completion, loop timeout, Alert.stop, runtime abort, or playback error.",
      "volume": "alertVolumeMode keep leaves media volume unchanged. set applies alertVolumePercentValue 0..100 to the public Media stream. alertRestoreVolume defaults true and restores on completion, timeout, Alert.stop, runtime stop, permission pause, preview cleanup, service exit, or app exit.",
      "outputs": "Optional outputs use alertSaveSuccess/alertSuccessVariableName as Boolean and alertSaveError/alertErrorVariableName as String.",
      "runtime": "Runtime uses public Android MediaPlayer, AudioManager media stream volume, persisted SAF grants, and public RingtoneManager URIs only. It does not create a foreground notification.",
      "privacy": "Run logs report only mode/source/success/error codes and do not log audio contents, raw file paths, full sensitive URIs, or selected audio labels."
    },
    "reader": {
      "readerModeValues": [
        "speakText",
        "readUI",
        "readOCR",
        "stop"
      ],
      "readerTextSourceModeValues": [
        "literal",
        "variable"
      ],
      "readerUiReadModeValues": [
        "text",
        "content_description",
        "both"
      ],
      "readerSpeechExecutionValues": [
        "wait",
        "concurrent"
      ],
      "readerVoiceModeValues": [
        "default",
        "select"
      ],
      "speakText": "Reader.speakText requires literal readerTextValue or one prior String readerTextVariableName. It does not request screen capture.",
      "readUI": "Reader.readUI reads visible Accessibility text/content descriptions from the active app, skips ScriptTap nodes, and can optionally filter to readerLeft/readerTop/readerRight/readerBottom or reader*Value Integer fields. It does not request screen capture, including for area selection.",
      "readOCR": "Reader.readOCR requires a selected reader area and is the only Reader mode that uses screen capture. It uses ScriptTap's existing OCR engine/capture flow and does not create a separate OCR path.",
      "stop": "Reader.stop stops only ScriptTap-owned Reader TTS for the active runtime.",
      "limits": "readerMaxCharactersValue defaults to 3000 and is capped at 10000. readerSpeechRatePercentValue and readerPitchPercentValue default to 100 and are capped to 50..200.",
      "exclusiveListening": "readerExclusiveListeningEnabled defaults false. When true, readerExclusiveListeningAlarmVolumePercentValue is a literal 1..100 percent defaulting to 75; runtime mutes Media, routes Reader TTS through Alarm, restores Media/Alarm on script/Test stop, and keeps pending crash restore state for next app/service start.",
      "outputs": "Optional outputs use readerSaveText/readerTextOutputVariableName as String, readerSaveSuccess/readerSuccessVariableName as Boolean, and readerSaveError/readerErrorVariableName as String. Unavailable selected TTS engine/language/voice returns an error.",
      "runtime": "Android-local only: public TextToSpeech APIs, installed engines/voices, Accessibility nodes for readUI, and existing on-device OCR for readOCR. Speech defaults to wait. Read modes capture the expected foreground app before extraction/capture when Stop If App Changes is enabled, stop on app_changed before stale speech, and never treat ScriptTap as the target app. Exclusive Listening does not control or pause other apps directly and does not upload website content. No cloud TTS, AI voice, microphone, voice cloning, root, ADB, private Android APIs, foreground notification, or speaking while locked."
    },
    "display": {
      "displayModeValues": [
        "once_text",
        "concurrent_text"
      ],
      "displayTextSourceModeValues": [
        "literal",
        "variable"
      ],
      "displayFontValues": [
        "system",
        "serif",
        "mono",
        "readable_wide"
      ],
      "displayBoxStyleValues": [
        "plain",
        "translucent",
        "outline",
        "highlight"
      ],
      "displayAlignmentValues": [
        "left",
        "center",
        "right"
      ],
      "area": "Display requires a selected area through displayLeft/displayTop/displayRight/displayBottom or matching display*Value Integer fields. Missing or invalid area is saveable but incomplete.",
      "source": "Literal source uses displayTextValue and blank text is valid. Variable source uses displayVariableName and may read any supported prior variable type as display text.",
      "once": "Display.once.text resolves source once, blocks only its branch for displayLifespanMsValue, defaults lifespan to 5000 ms, then removes the overlay.",
      "concurrent": "Display.concurrent.text returns immediately, keeps one active overlay per run generation and command id, refreshes from source no faster than displayRefreshRateMsValue with values below 100 ms clamped to 100 ms, de-dupes repeated loop executions for the same command, and skips view updates when text is unchanged.",
      "rolling": "Concurrent-only displayRollingUpdatesEnabled defaults false. When true, displayRollingScrollMsValue defaults to 1000 ms and clamps below 100 ms to 100 ms; malformed values are incomplete. Runtime appends changed nonblank values at the bottom, ignores blank/duplicate values without clearing prior captions, bounds visible history, and shares one fixed-duration scroll animation window for accepted bursts instead of creating animation backlog.",
      "stop": "Optional displayStopBooleanVariableName must be a prior Boolean. True removes the active concurrent Display for that command; false alone does not create a box.",
      "runtime": "Touch-through ScriptTap-owned text overlay above the target app and below all ScriptTap UI, selectors, modals, Main Overlay, Active, Tutorial, Bug Panel, and PromptMe Mini. No output variables, no capture, no network/cloud/website upload, no custom/bundled/downloadable fonts, no root/ADB/private APIs, and logs do not include full displayed text."
    },
    "touch_down": {
      "fields": "Uses touchXValue and touchYValue as Integer literals or prior Integer variables.",
      "structure": "TouchDown opens an active Touch block and must be matched by End Touch. The editor auto-inserts End Touch after palette-created TouchDown.",
      "lanes": "Consumes one of at most 3 held TouchDown slots inside the global 5-lane script gesture session.",
      "runtime": "Starts a held Accessibility stroke. Every later gesture dispatch continues the stroke until End Touch."
    },
    "touch_move": {
      "fields": "Uses touchXValue, touchYValue, and touchMoveTimeMsValue as Integer literals or prior Integer variables. Total Move Time defaults to exactly 200 ms.",
      "structure": "TouchMove is valid only inside an active enabled TouchDown..End Touch block.",
      "runtime": "Runtime starts the linear move immediately and advances it concurrently with following commands for the full Total Move Time. The owning TouchDown stroke remains held. Negative or malformed timing is incomplete."
    },
    "end_touch": {
      "fields": "No editable fields.",
      "structure": "Ends the innermost active TouchDown block. It must not cross IF, Loop, ForEachIndex, Exclusive, or Concurrent boundaries.",
      "runtime": "Releases the active held Touch lane."
    },
    "delay_until": {
      "delayUntilModeValues": [
        "time",
        "image",
        "ui_element",
        "pixel",
        "active_app",
        "variable"
      ],
      "pollInterval": "delayUntilImageCheckMs is the non-Time poll interval and is clamped to 0..60000 ms.",
      "image": "Image mode uses assetNames[] for one or more selected image assets. Runtime waits until any selected asset appears inside the selected area.",
      "uiElement": "Uses Accessibility nodes only, with uiFindQuery, uiFindTarget any/text/content_description/view_id/class_name, uiFindMatchMode exact/contains/regex, uiFindNth, and optional enabled/clickable/visible/package filters. It uses no screen-image text recognition.",
      "pixel": "Uses x/y plus variableValue as a Hex literal or prior Hex variable. Fuzzy matching uses confidencePercent 1..100, defaulting to 90 when absent; there is no public tolerance field.",
      "activeApp": "Uses devicePackageName. Runtime waits for the current foreground package and fails/skips deterministically if no active package source is available.",
      "variable": "Uses a prior variable selected by variableName and variableType. Boolean and Hex use equals. Integer supports =, <>, >, >=, <, <=. String uses uiFindMatchMode exact/contains/regex."
    },
    "img_find": {
      "outputModeValues": [
        "standard",
        "classify"
      ],
      "findModes": "findAll=false is Find 1st and can write found Boolean plus optional X/Y. findAll=true is Find All and can write Integer Count plus X/Y arrays. imgFindMaxResultsValue accepts a 1..4096 Integer literal or prior Integer variable, defaults to 255, and caps saved Find All rows.",
      "matching": "ImgFind uses confidencePercent for positive matching and negativeConfidencePercent for negative-asset rejection. Both are deterministic per-channel RGB confidence values. No public tolerance field is used.",
      "outputs": "Standard Find 1st uses variableName as Boolean found output. Standard Find All Count uses useCountOutput=true plus outputCountVariableName as Integer Count; older scripts may carry this Count alias in variableName. xVariableName/yVariableName are Integer outputs for Find 1st and Integer arrays for Find All.",
      "classify": "Classify Find All can write Integer Count through outputCountVariableName plus imgFindClassifyOutputIndexVariable, imgFindClassifyOutputXVariable, and imgFindClassifyOutputYVariable arrays, capped by imgFindMaxResultsValue. imgFindBooleanArrayName writes one Boolean row per selected asset.",
      "runtime": "No capture or no found writes Count 0 when Count is enabled. Find All outputs are ordered top-to-bottom then left-to-right."
    },
    "ui_find": {
      "targetValues": [
        "any",
        "text",
        "content_description",
        "view_id",
        "class_name"
      ],
      "matchModeValues": [
        "exact",
        "contains",
        "regex"
      ],
      "tapModeValues": [
        "node_click",
        "screen_tap"
      ],
      "accessibilityOnly": "UI Find uses exposed Android Accessibility nodes only. It does not use image matching, screen capture, root, ADB, extra permissions, or screen-image text recognition.",
      "activeAppRule": "UI Find searches current exposed application windows. A nonempty uiFindPackageFilter limits matching to that package; an empty filter preserves cross-window matching. Use Open App before UI Find when the target app is not already exposed.",
      "tapRule": "The UI uses the shared Tap Target / Set Variables block: Center maps to uiFindTapMode=node_click, and Set Coordinates maps to uiFindTapMode=screen_tap with clickTargetXValue/clickTargetYValue after a match.",
      "editorSelector": "The target-square beside Query in UI Find and Delay Until UI Element opens Select UI Element. With exactly two exposed application windows it has two equal independently scrolling surfaces in screen-position order, separated by the existing divider. Selecting a row returns exact Query, Target, Match=Exact, package filter, and package-local Nth to the command modal."
    },
    "ocr": {
      "ocrModeValues": [
        "find",
        "read"
      ],
      "querySourceModeValues": [
        "literal",
        "variable"
      ],
      "matchModeValues": [
        "exact",
        "contains",
        "regex"
      ],
      "textUnitValues": [
        "line",
        "word"
      ],
      "areaRequired": "OCR always requires a selected area through ocrLeft/ocrTop/ocrRight/ocrBottom or matching ocr*Value Integer fields. Do not default OCR to full screen.",
      "find": "OCR.find requires a literal ocrQueryValue or prior String ocrQueryVariableName, ocrMatchMode, ocrTextUnit, Find 1st/Find All fields, and either clickIfFound=true or setVariables=true.",
      "read": "OCR.read requires ocrReadOutputVariableName as one String output. Optional outputs use ocrReadSaveSuccess/ocrReadSuccessVariableName as Boolean and ocrReadSaveError/ocrReadErrorVariableName as String.",
      "outputs": "OCR.find current modal outputs use generic variableName Boolean plus xVariableName/yVariableName Integer under Set Variables. Use b[] or another intentionally Boolean scratch letter for Boolean outputs; do not put Boolean OCR outputs into z[] unless z is intentionally typed Boolean.",
      "runtime": "Uses bundled on-device ML Kit Text Recognition v2 Latin through ScriptTap's existing screen-capture permission/session path. It never sends screenshots or recognized text off-device, never uses camera/root/ADB/private APIs, and cannot request screen capture from a background routine trigger."
    },
    "mod_variable": {
      "modModeValues": [
        "basic",
        "expression",
        "array"
      ],
      "basic": "Basic mode writes modTargetVariableName from modLeftValue, modOperator, and modRightValue. modUseHex applies only to Basic mode.",
      "expression": "Expression mode writes one Integer modTargetVariableName from modExpression using normal prior Integer variable tokens.",
      "array": "Array mode attempts Integer-family writes selected by modArrayTargetLetter from modArrayTargetStartIndexValue for modArrayCountValue offsets. Count 0 attempts no writes; calculated counts outside 0 through 4096 become zero.",
      "arrayInputs": "Array mode reads one to three prior matching Integer families from modArrayInputAArrayLetter/modArrayInputAStartIndexValue, modArrayInputBArrayLetter/modArrayInputBStartIndexValue, and modArrayInputCArrayLetter/modArrayInputCStartIndexValue.",
      "arrayExpression": "Array mode expressions may use only fixed aliases a, b, c for populated input rows and i for the 1-based offset. Do not use normal variable tokens such as i1 or n1 inside the array expression.",
      "arrayAtomic": "Array mode evaluates each attempted offset in declaration order. Missing or invalid Integer reads return zero and invalid writes are discarded while execution continues. It has no file, network, clipboard, screen, or tap side effects."
    },
    "color_classify": {
      "colorClassifyModeValues": [
        "single",
        "array"
      ],
      "targetSourceModeValues": [
        "rows",
        "hex_array"
      ],
      "arrayMode": "colorClassifyMode array reads sourceArrayLetter as a prior matching Hex family from sourceArrayStartIndexValue for sourceArrayCountValue offsets and maps each offset to outputArrayLetter beginning at outputArrayLetter1. Count 0 attempts no writes; calculated counts outside 0 through 4096 become zero. Missing or invalid Hex reads use 000000 and invalid output writes are discarded.",
      "rows": "targetSourceMode rows uses fixed targets[] rows. Each row has targetColor as a Hex literal or prior Hex variable plus resultValue typed by outputType.",
      "hexArray": "targetSourceMode hex_array reads targetArrayLetter as a prior matching Hex family plus targetArrayStartIndexValue and targetArrayCountValue as Integer literals or prior Integer variables. It scans ascending sparse slots and applies confidencePercent RGB matching; missing or invalid reads use Hex 000000.",
      "outputs": "Single mode writes outputVariableName. Array mode attempts outputArrayLetter writes by source-segment offset starting at slot 1; invalid writes are discarded without compacting later offsets. Integer output writes the matched absolute 1-based target slot in hex_array target mode or the row resultValue in rows target mode. String output writes the matched Hex value in hex_array target mode or the row resultValue in rows target mode. No match writes noMatchValue.",
      "dynamicGrouping": "Use ColorClassify with targetSourceMode hex_array for confidence-aware dynamic color-class lookup. Use colorClassifyMode array when classifying a sampled Hex array segment in one command."
    },
    "array_set": {
      "valueSourceModeValues": [
        "literal",
        "variable"
      ],
      "arraySetModeValues": [
        "single",
        "bulk",
        "fill_range",
        "sequence"
      ],
      "literal": "ArraySet literal writes literalValue to exactly one typed array slot at arrayLetter[indexValue].",
      "variable": "ArraySet variable reads sourceVariableName as the selected variableType and writes exactly one typed array slot at arrayLetter[indexValue].",
      "bulkEntries": "When arraySetMode is bulk, use structured bulkEntries[] rows with enabled, indexValue, valueSourceMode, literalValue, and sourceVariableName. Saved/readback JSON must use rows, not paste shorthand.",
      "fillRange": "When arraySetMode is fill_range, use startIndexValue and lengthValue as the contiguous 1-based destination range. valueSourceMode selects literalValue or sourceVariableName, and the same typed value is written to every slot in the range.",
      "sequence": "When arraySetMode is sequence, ArraySet writes Integer slots only. startIndexValue and lengthValue select the contiguous range; startValue and stepValue are signed Integer literals or prior Integer variables used to generate each slot value.",
      "rangeAtomic": "fill_range and sequence use lengthValue as an attempted-write count. Count 0 attempts no writes; calculated counts outside 0 through 4096 become zero. Invalid or out-of-domain individual writes are discarded.",
      "bulkAtomic": "Bulk mode evaluates enabled rows in declaration order. Invalid, incompatible, or out-of-domain writes are discarded without stopping later valid rows. Maximum enabled rows is 255.",
      "setVariableRelation": "Use ArraySet single mode for one attempted slot, bulk mode for structured same-type row attempts, fill_range for repeated writes, and sequence for Integer progressions after a matching typed-family declaration."
    },
    "for_each_index": {
      "bounds": "startValue, endValue, and stepValue accept signed Integer literals or prior Integer variable tokens. They resolve once when the loop starts.",
      "zeroStep": "stepValue 0 is normalized to 1 before the loop state is created.",
      "indexOutput": "indexVariableName is an Integer output variable and is set to the current value at the start of each pass.",
      "delay": "loopDelayMsValue resolves before each End ForEachIndex repeat sleep.",
      "variableBackedExample": {
        "type": "for_each_index",
        "indexVariableName": "i9",
        "startValue": "i1",
        "endValue": "i2",
        "stepValue": "i3",
        "loopDelayMsValue": "0"
      },
      "variableBackedExampleRequirement": "i1, i2, and i3 must already be Integer variables before the loop."
    },
    "append_points": {
      "appendPointsModeValues": [
        "rows",
        "grid"
      ],
      "appendPointsOrderValues": [
        "row_major",
        "column_major"
      ],
      "rowMode": "appendPointsMode rows uses points[] rows with x, y, and enabled fields. Enabled point coordinates are signed Integer literals or prior Integer variables.",
      "gridMode": "appendPointsMode grid generates X/Y coordinate arrays from supplied Integer inputs only: appendPointsRowsValue, appendPointsColumnsValue, appendPointsLeftValue, appendPointsTopValue, appendPointsXPitchValue, appendPointsYPitchValue, appendPointsXOffsetValue, appendPointsYOffsetValue, and appendPointsOrder. It does not detect grids, read the screen, match images, or infer game state.",
      "gridAtomic": "Grid mode uses row/column counts as attempted-write counts. Count 0 attempts no X/Y writes; calculated counts outside 0 through 4096 become zero. Invalid individual X/Y writes are discarded and execution continues.",
      "outputs": "Both modes write xArrayLetter[] and yArrayLetter[] Integer slots starting at countVariableName+1, then update countVariableName. Optional outputAddedCountVariableName is Integer and outputSuccessVariableName is Boolean."
    },
    "array_segment_scan": {
      "scanModeValues": [
        "first_non_empty",
        "first_empty",
        "first_equal",
        "count_equal",
        "count_non_empty",
        "run_equal",
        "run_boundary",
        "aggregate"
      ],
      "matchValueSourceModeValues": [
        "none",
        "literal",
        "variable"
      ],
      "run_equal": "When matchValueSourceMode is none, run_equal finds the first non-empty run using emptyValue in scanDirection.",
      "explicitMatch": "When matchValueSourceMode is literal or variable, run_equal finds the first run equal to that explicit match, including an explicit match equal to the configured empty value. first_equal and count_equal require literal or variable.",
      "activeMask": "For scalar scan modes, optional activeMaskMode on reads a prior matching Boolean activeMaskArrayLetter family and skips mask-false slots. Missing, invalid, incompatible, or out-of-domain mask reads return Boolean False.",
      "run_boundary": "scanMode run_boundary identifies contiguous matching runs after active-mask filtering. With matchValueSourceMode none it uses first non-empty run semantics; with literal or variable it matches that explicit typed value. outputIndexVariableName writes the selected run first index, outputLastIndexVariableName / outputValueVariableName writes selected run last index as Integer, outputCountVariableName writes total matching-run count, and outputRunLengthVariableName / outputAddedCountVariableName writes selected run length. Enabled last-index/run-length outputs require explicit variable names; blank aliases remain incomplete.",
      "aggregate": "scanMode aggregate groups non-empty keys from arrayLetter in first-seen order over startIndexValue plus lengthValue. It uses matching outputCountArrayLetter Integer, optional Boolean mask, optional Integer sums, and optional typed key output families. Count 0 attempts no group writes; calculated counts outside 0 through 4096 become zero and invalid output writes are discarded."
    },
    "device_intent_action": {
      "deviceIntentModeValues": [
        "view_uri",
        "map_uri",
        "settings",
        "app_details",
        "email",
        "sms",
        "dial",
        "share_text"
      ],
      "deviceIntentSettingsActionValues": [
        "main",
        "display",
        "sound",
        "notifications",
        "accessibility",
        "applications",
        "location",
        "date_time",
        "input_method",
        "language",
        "battery_saver"
      ],
      "safety": "Foreground intents only. Email/SMS/dial/share prefill user-visible UI; they never silently send, call, share files, scrape contacts, read notifications, use root/ADB, or accept raw Android settings action strings."
    },
    "device_system_nav": {
      "deviceSystemNavActionValues": [
        "back",
        "home",
        "recents",
        "notifications",
        "quick_settings",
        "escape",
        "scroll_top",
        "scroll_bottom"
      ],
      "unavailable": "Enter and Search are intentionally not public action values because Android Accessibility has no safe deterministic key path for them."
    },
    "http": {
      "httpMethodValues": [
        "get",
        "post"
      ],
      "sourceModeValues": [
        "literal",
        "variable"
      ],
      "urlRule": "Use httpUrlSourceMode literal with httpUrlValue, or variable with one prior String httpUrlVariableName. URLs may use http:// or https://, or a bare domain that defaults to https://; a host is required.",
      "headersRule": "httpHeaders[] is a row array. Each row requires a standard header token name and either literal value or one prior String variableName when valueSourceMode=variable.",
      "bodyRule": "POST may use literal httpBodyValue or one prior String httpBodyVariableName. GET ignores body fields. Blank POST literal body is allowed.",
      "timeoutRule": "httpTimeoutMs is clamped to 1000..60000 ms; default is 10000.",
      "outputs": "Optional outputs are httpSaveSuccess/httpSuccessVariableName Boolean, httpSaveStatus/httpStatusVariableName Integer, httpSaveBody/httpBodyOutputVariableName String, and httpSaveError/httpErrorVariableName String.",
      "runtime": "Client-side only. Follows redirects up to 5. Response/error body text is capped at 256KB with a truncation note. Runtime logs method, host/path, status, elapsed, success, and short error only; it does not log full headers or body."
    }
  },
  "minimalPackage": {
    "format": "scripttap-json-script",
    "version": 1,
    "kind": "script_package",
    "packageId": "pkg_example",
    "createdAtMs": 0,
    "source": "ai_generated",
    "rootScriptId": "script_root",
    "requirements": {
      "commandCatalogVersion": 1,
      "requiresUserCompletion": false,
      "requiresAssets": false
    },
    "folders": [],
    "scripts": [
      {
        "id": "script_root",
        "name": "New Script",
        "folderId": "",
        "disabled": false,
        "commands": []
      }
    ],
    "assetRequirements": [],
    "notes": ""
  }
}
