shortcuts-playground 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +54 -0
  2. package/agents/shortcut-builder.md +172 -0
  3. package/agents/shortcut-remixer.md +239 -0
  4. package/commands/build-shortcut.md +32 -0
  5. package/commands/remix-shortcut.md +34 -0
  6. package/opencode.json +30 -0
  7. package/package.json +47 -0
  8. package/plugin/validator.ts +179 -0
  9. package/skills/shortcuts-playground/ACTIONS.md +713 -0
  10. package/skills/shortcuts-playground/APPINTENTS.md +2760 -0
  11. package/skills/shortcuts-playground/AUTOMATION_TRIGGERS.md +133 -0
  12. package/skills/shortcuts-playground/BEST_PRACTICES.md +357 -0
  13. package/skills/shortcuts-playground/CHANGELOG.md +585 -0
  14. package/skills/shortcuts-playground/CONTROL_FLOW.md +777 -0
  15. package/skills/shortcuts-playground/DATE_TIME.md +75 -0
  16. package/skills/shortcuts-playground/EXAMPLES.md +738 -0
  17. package/skills/shortcuts-playground/FILTERS.md +697 -0
  18. package/skills/shortcuts-playground/HEALTHKIT.md +317 -0
  19. package/skills/shortcuts-playground/ICONS_AND_COLORS.md +89 -0
  20. package/skills/shortcuts-playground/JAVASCRIPT_WEBPAGE.md +51 -0
  21. package/skills/shortcuts-playground/PARAMETER_TYPES.md +1117 -0
  22. package/skills/shortcuts-playground/PLIST_FORMAT.md +296 -0
  23. package/skills/shortcuts-playground/README.md +77 -0
  24. package/skills/shortcuts-playground/SKILL.md +459 -0
  25. package/skills/shortcuts-playground/THIRD_PARTY_ACTIONS.md +77 -0
  26. package/skills/shortcuts-playground/TOOLKIT_SNAPSHOT.md +62 -0
  27. package/skills/shortcuts-playground/URL_SCHEMES.md +59 -0
  28. package/skills/shortcuts-playground/VARIABLES.md +569 -0
  29. package/skills/shortcuts-playground/assets/shortcuts-small.svg +14 -0
  30. package/skills/shortcuts-playground/assets/shortcuts.png +0 -0
  31. package/skills/shortcuts-playground/data/healthkit-ios26.2-reference.json +2603 -0
  32. package/skills/shortcuts-playground/data/macos27-shortpy-grounding.json +4085 -0
  33. package/skills/shortcuts-playground/data/macos27-workflow-trigger-samples.json +1 -0
  34. package/skills/shortcuts-playground/data/shortcuts-glyph-synonyms.json +5102 -0
  35. package/skills/shortcuts-playground/data/shortcuts-icon-colors.json +107 -0
  36. package/skills/shortcuts-playground/data/shortcuts-official-glyph-mapping.json +509 -0
  37. package/skills/shortcuts-playground/data/toolkit-v63-tool-ids.json +1806 -0
  38. package/skills/shortcuts-playground/data/toolkit-v78-first-party-enum-cases.json +1 -0
  39. package/skills/shortcuts-playground/data/toolkit-v78-first-party-parameter-keys.json +1 -0
  40. package/skills/shortcuts-playground/data/toolkit-v78-ios27-tool-ids.json +1222 -0
  41. package/skills/shortcuts-playground/data/toolkit-v78-tool-ids.json +2745 -0
  42. package/skills/shortcuts-playground/data/toolkit-v78-trigger-parameter-keys.json +1051 -0
  43. package/skills/shortcuts-playground/golden-shortcuts/index.jsonl +19 -0
  44. package/skills/shortcuts-playground/golden-shortcuts/xml/1be4dde95b794253bf82438e201b33e7.xml +174 -0
  45. package/skills/shortcuts-playground/golden-shortcuts/xml/2e0fb675e45948aaacee7e534f910492.xml +1016 -0
  46. package/skills/shortcuts-playground/golden-shortcuts/xml/332c12a0060043b388b22b806be7ab58.xml +1924 -0
  47. package/skills/shortcuts-playground/golden-shortcuts/xml/3dd4ee24e43f464f92adaa70a0311eaa.xml +1177 -0
  48. package/skills/shortcuts-playground/golden-shortcuts/xml/4d102301c6e646faa7a8a221f4f4ec98.xml +321 -0
  49. package/skills/shortcuts-playground/golden-shortcuts/xml/51cc4e26d1044893a0c3f2f3630cf2d2.xml +1933 -0
  50. package/skills/shortcuts-playground/golden-shortcuts/xml/623e7f1ca5f948e2bd53811fec63e544.xml +136 -0
  51. package/skills/shortcuts-playground/golden-shortcuts/xml/6a18b76843ac45c384ac3400f3740997.xml +1791 -0
  52. package/skills/shortcuts-playground/golden-shortcuts/xml/71f0cacb0f604b399b76c5dcb7286e7c.xml +2228 -0
  53. package/skills/shortcuts-playground/golden-shortcuts/xml/807525ed9f974829bc8494defac923a8.xml +259 -0
  54. package/skills/shortcuts-playground/golden-shortcuts/xml/8ab0d39826ab4249be58672763caa3ba.xml +420 -0
  55. package/skills/shortcuts-playground/golden-shortcuts/xml/91c45fd3fd3b427897d9ba485efb1227.xml +800 -0
  56. package/skills/shortcuts-playground/golden-shortcuts/xml/97be626bb25c41709d175646a7f6d8f2.xml +424 -0
  57. package/skills/shortcuts-playground/golden-shortcuts/xml/ae59e10d409348f9bd33894f03f9beb4.xml +568 -0
  58. package/skills/shortcuts-playground/golden-shortcuts/xml/afa83b6be811483b9c32189c41eb9312.xml +391 -0
  59. package/skills/shortcuts-playground/golden-shortcuts/xml/cfdb083b857e4ac189629fa386d27cdc.xml +193 -0
  60. package/skills/shortcuts-playground/golden-shortcuts/xml/e01cebe192d64b2fbca80204d03d92ab.xml +111 -0
  61. package/skills/shortcuts-playground/golden-shortcuts/xml/ef669bcf8bad489d9ef4b88bfaf5772f.xml +780 -0
  62. package/skills/shortcuts-playground/golden-shortcuts/xml/f44f5caf5e3e48d4817e73af450c4404.xml +570 -0
  63. package/skills/shortcuts-playground/scripts/generate_healthkit_reference.py +394 -0
  64. package/skills/shortcuts-playground/scripts/lookup_action_grounding.py +1164 -0
  65. package/skills/shortcuts-playground/scripts/select_shortcut_icon_color.py +597 -0
  66. package/skills/shortcuts-playground/scripts/test_random_mixed_shortcuts.py +1067 -0
  67. package/skills/shortcuts-playground/scripts/test_wiring_regressions.py +2247 -0
  68. package/skills/shortcuts-playground/scripts/validate_shortcut.py +4526 -0
@@ -0,0 +1,1067 @@
1
+ #!/usr/bin/env python3
2
+ """Generate and validate random mixed-action shortcut stress tests.
3
+
4
+ This harness builds brand-new shortcuts with randomized parameters and optional
5
+ modules, enforces a minimum number of distinct actions per shortcut, validates
6
+ them with retries, and writes documentation artifacts for auditability.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import datetime as dt
13
+ import json
14
+ import os
15
+ import plistlib
16
+ import random
17
+ import shutil
18
+ import subprocess
19
+ import uuid
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Any, Optional
23
+
24
+ from validate_shortcut import (
25
+ load_allowed_ids,
26
+ load_future_parameter_key_reasons,
27
+ load_future_toolkit_id_reasons,
28
+ load_icon_metadata,
29
+ load_toolkit_parameter_boolean_keys,
30
+ load_toolkit_parameter_enum_cases,
31
+ load_toolkit_parameter_schemas,
32
+ resolve_target_macos_major,
33
+ resolve_target_platform,
34
+ validate,
35
+ )
36
+
37
+ PLACEHOLDER = "\uFFFC"
38
+
39
+ WEATHER_DETAILS = [
40
+ "Condition",
41
+ "Temperature",
42
+ "High",
43
+ "Low",
44
+ "Humidity",
45
+ "UV Index",
46
+ "Sunrise Time",
47
+ "Sunset Time",
48
+ "Wind Speed",
49
+ "Visibility",
50
+ ]
51
+
52
+ ENDPOINTS = [
53
+ ("https://httpbin.org/get?case={i}", "url"),
54
+ ("https://api.agify.io?name=alex", "age"),
55
+ ("https://api.genderize.io?name=alex", "gender"),
56
+ ("https://catfact.ninja/fact", "fact"),
57
+ ("https://jsonplaceholder.typicode.com/todos/1", "title"),
58
+ ]
59
+
60
+ EXTRA_MODULES = ("detect.link", "text.changecase", "base64")
61
+
62
+
63
+ @dataclass
64
+ class BuildResult:
65
+ workflow: dict[str, Any]
66
+ distinct_actions: int
67
+ action_ids: list[str]
68
+ extras_used: list[str]
69
+
70
+
71
+ def seeded_uuid(seed: str) -> str:
72
+ return str(uuid.uuid5(uuid.NAMESPACE_DNS, seed)).upper()
73
+
74
+
75
+ def action_output(output_uuid: str, output_name: str) -> dict[str, Any]:
76
+ return {
77
+ "WFSerializationType": "WFTextTokenAttachment",
78
+ "Value": {
79
+ "Type": "ActionOutput",
80
+ "OutputUUID": output_uuid,
81
+ "OutputName": output_name,
82
+ },
83
+ }
84
+
85
+
86
+ def variable_wrapper_action_output(output_uuid: str, output_name: str) -> dict[str, Any]:
87
+ return {
88
+ "Type": "Variable",
89
+ "Variable": action_output(output_uuid, output_name),
90
+ }
91
+
92
+
93
+ def token_string_action_output(output_uuid: str, output_name: str) -> dict[str, Any]:
94
+ return {
95
+ "WFSerializationType": "WFTextTokenString",
96
+ "Value": {
97
+ "string": PLACEHOLDER,
98
+ "attachmentsByRange": {
99
+ "{0, 1}": {
100
+ "Type": "ActionOutput",
101
+ "OutputUUID": output_uuid,
102
+ "OutputName": output_name,
103
+ }
104
+ },
105
+ },
106
+ }
107
+
108
+
109
+ def token_string_from_parts(parts: list[Any]) -> dict[str, Any]:
110
+ text = ""
111
+ attachments: dict[str, dict[str, Any]] = {}
112
+ for part in parts:
113
+ if isinstance(part, str):
114
+ text += part
115
+ continue
116
+ start = len(text)
117
+ text += PLACEHOLDER
118
+ attachments[f"{{{start}, 1}}"] = part
119
+ payload: dict[str, Any] = {"string": text}
120
+ if attachments:
121
+ payload["attachmentsByRange"] = attachments
122
+ return {"WFSerializationType": "WFTextTokenString", "Value": payload}
123
+
124
+
125
+ def prompt_comment(case_name: str) -> str:
126
+ return (
127
+ "Shortcuts generated by Shortcuts Playground. May contain mistakes. "
128
+ "Always check the shortcut's actions first.\n\n"
129
+ "This shortcut was created via the following user prompt:\n\n"
130
+ f"> Random mixed-action stress test for {case_name}"
131
+ )
132
+
133
+
134
+ def make_workflow(name: str, actions: list[dict[str, Any]]) -> dict[str, Any]:
135
+ return {
136
+ "WFWorkflowActions": actions,
137
+ "WFWorkflowClientVersion": "2700.0.4",
138
+ "WFWorkflowHasOutputFallback": False,
139
+ "WFWorkflowIcon": {
140
+ "WFWorkflowIconGlyphNumber": 61440,
141
+ "WFWorkflowIconStartColor": 431817727,
142
+ },
143
+ "WFWorkflowImportQuestions": [],
144
+ "WFWorkflowInputContentItemClasses": [],
145
+ "WFWorkflowMinimumClientVersion": 900,
146
+ "WFWorkflowMinimumClientVersionString": "900",
147
+ "WFWorkflowName": name,
148
+ "WFWorkflowOutputContentItemClasses": [],
149
+ "WFWorkflowTypes": [],
150
+ }
151
+
152
+
153
+ def append_os27_actions(
154
+ actions: list[dict[str, Any]],
155
+ case_name: str,
156
+ idx: int,
157
+ uid,
158
+ ) -> list[str]:
159
+ extras_used = ["os27.actions"]
160
+
161
+ os27_text_uuid = uid("os27-text")
162
+ os27_key_uuid = uid("os27-key")
163
+ os27_list_uuid = uid("os27-list")
164
+ os27_added_uuid = uid("os27-add-item")
165
+ os27_stored_uuid = uid("os27-stored-get")
166
+ os27_group_uuid = uid("os27-otherwise-if-group")
167
+ os27_selected_uuid = uid("os27-selected-text")
168
+ os27_screen_uuid = uid("os27-on-screen")
169
+ os27_find_vpns_uuid = uid("os27-find-vpns")
170
+ os27_vpn_uuid = uid("os27-current-vpn")
171
+
172
+ conditional_input = variable_wrapper_action_output(os27_added_uuid, "List")
173
+ actions.extend(
174
+ [
175
+ {
176
+ "WFWorkflowActionIdentifier": "is.workflow.actions.comment",
177
+ "WFWorkflowActionParameters": {
178
+ "WFCommentActionText": (
179
+ "OS 27 module: Add Item to List, Stored Content, "
180
+ "Otherwise If, selected text, on-screen context, and VPN."
181
+ )
182
+ },
183
+ },
184
+ {
185
+ "WFWorkflowActionIdentifier": "is.workflow.actions.gettext",
186
+ "WFWorkflowActionParameters": {
187
+ "UUID": os27_text_uuid,
188
+ "WFTextActionText": f"{case_name} OS 27 stored value",
189
+ },
190
+ },
191
+ {
192
+ "WFWorkflowActionIdentifier": "is.workflow.actions.gettext",
193
+ "WFWorkflowActionParameters": {
194
+ "UUID": os27_key_uuid,
195
+ "WFTextActionText": f"sp27-random-{idx:03d}",
196
+ },
197
+ },
198
+ {
199
+ "WFWorkflowActionIdentifier": "is.workflow.actions.setstoredcontent",
200
+ "WFWorkflowActionParameters": {
201
+ "WFInput": token_string_action_output(os27_text_uuid, "Text"),
202
+ "WFStoredContentKey": token_string_action_output(os27_key_uuid, "Text"),
203
+ "WFStoredContentGlobalValue": False,
204
+ },
205
+ },
206
+ {
207
+ "WFWorkflowActionIdentifier": "is.workflow.actions.getstoredcontent",
208
+ "WFWorkflowActionParameters": {
209
+ "UUID": os27_stored_uuid,
210
+ "WFStoredContentKey": f"sp27-random-{idx:03d}",
211
+ "WFStoredContentGlobalValue": False,
212
+ },
213
+ },
214
+ {
215
+ "WFWorkflowActionIdentifier": "is.workflow.actions.deletestoredcontent",
216
+ "WFWorkflowActionParameters": {
217
+ "WFStoredContentKey": f"sp27-random-{idx:03d}",
218
+ "WFStoredContentGlobalValue": False,
219
+ },
220
+ },
221
+ {
222
+ "WFWorkflowActionIdentifier": "is.workflow.actions.list",
223
+ "WFWorkflowActionParameters": {"UUID": os27_list_uuid},
224
+ },
225
+ {
226
+ "WFWorkflowActionIdentifier": "is.workflow.actions.additemtolist",
227
+ "WFWorkflowActionParameters": {
228
+ "UUID": os27_added_uuid,
229
+ "WFListItem": "Apple",
230
+ "WFListVariable": action_output(os27_list_uuid, "List"),
231
+ "WFInsertPosition": "End",
232
+ },
233
+ },
234
+ {
235
+ "WFWorkflowActionIdentifier": "is.workflow.actions.comment",
236
+ "WFWorkflowActionParameters": {
237
+ "WFCommentActionText": (
238
+ "- If checks the list created above for Apple\n"
239
+ "- Otherwise If checks the same list for Orange"
240
+ )
241
+ },
242
+ },
243
+ {
244
+ "WFWorkflowActionIdentifier": "is.workflow.actions.conditional",
245
+ "WFWorkflowActionParameters": {
246
+ "GroupingIdentifier": os27_group_uuid,
247
+ "WFControlFlowMode": 0,
248
+ "WFCondition": 99,
249
+ "WFInput": conditional_input,
250
+ "WFConditionalActionString": "Apple",
251
+ },
252
+ },
253
+ {
254
+ "WFWorkflowActionIdentifier": "is.workflow.actions.nothing",
255
+ "WFWorkflowActionParameters": {"UUID": uid("os27-if-apple-nothing")},
256
+ },
257
+ {
258
+ "WFWorkflowActionIdentifier": "is.workflow.actions.conditional",
259
+ "WFWorkflowActionParameters": {
260
+ "GroupingIdentifier": os27_group_uuid,
261
+ "WFControlFlowMode": 1,
262
+ "WFCondition": 99,
263
+ "WFInput": conditional_input,
264
+ "WFConditionalActionString": "Orange",
265
+ },
266
+ },
267
+ {
268
+ "WFWorkflowActionIdentifier": "is.workflow.actions.nothing",
269
+ "WFWorkflowActionParameters": {"UUID": uid("os27-if-orange-nothing")},
270
+ },
271
+ {
272
+ "WFWorkflowActionIdentifier": "is.workflow.actions.conditional",
273
+ "WFWorkflowActionParameters": {
274
+ "GroupingIdentifier": os27_group_uuid,
275
+ "WFControlFlowMode": 1,
276
+ "WFInput": conditional_input,
277
+ },
278
+ },
279
+ {
280
+ "WFWorkflowActionIdentifier": "is.workflow.actions.nothing",
281
+ "WFWorkflowActionParameters": {"UUID": uid("os27-if-else-nothing")},
282
+ },
283
+ {
284
+ "WFWorkflowActionIdentifier": "is.workflow.actions.conditional",
285
+ "WFWorkflowActionParameters": {
286
+ "GroupingIdentifier": os27_group_uuid,
287
+ "WFControlFlowMode": 2,
288
+ },
289
+ },
290
+ {
291
+ "WFWorkflowActionIdentifier": "is.workflow.actions.getselectedtext",
292
+ "WFWorkflowActionParameters": {"UUID": os27_selected_uuid},
293
+ },
294
+ {
295
+ "WFWorkflowActionIdentifier": "is.workflow.actions.getonscreencontext",
296
+ "WFWorkflowActionParameters": {
297
+ "UUID": os27_screen_uuid,
298
+ "WFOnScreenContextScope": "Focused App Only",
299
+ "WFOnScreenContextLimitEnabled": True,
300
+ "WFOnScreenContextLimit": 3,
301
+ },
302
+ },
303
+ {
304
+ "WFWorkflowActionIdentifier": "is.workflow.actions.filter.vpns",
305
+ "WFWorkflowActionParameters": {
306
+ "UUID": os27_find_vpns_uuid,
307
+ "WFContentItemInputParameter": "Library",
308
+ "WFContentItemSortProperty": "Server Address",
309
+ "WFContentItemSortOrder": "A to Z",
310
+ "WFContentItemLimitEnabled": True,
311
+ "WFContentItemLimitNumber": 3,
312
+ "WFCompoundType": "1",
313
+ },
314
+ },
315
+ {
316
+ "WFWorkflowActionIdentifier": "is.workflow.actions.vpn.get",
317
+ "WFWorkflowActionParameters": {"UUID": os27_vpn_uuid},
318
+ },
319
+ {
320
+ "WFWorkflowActionIdentifier": "is.workflow.actions.showresult",
321
+ "WFWorkflowActionParameters": {
322
+ "Text": token_string_from_parts(
323
+ [
324
+ "OS 27 stored content: ",
325
+ {
326
+ "Type": "ActionOutput",
327
+ "OutputUUID": os27_stored_uuid,
328
+ "OutputName": "Stored Content",
329
+ },
330
+ " / selected: ",
331
+ {
332
+ "Type": "ActionOutput",
333
+ "OutputUUID": os27_selected_uuid,
334
+ "OutputName": "Selected Text",
335
+ },
336
+ " / screen: ",
337
+ {
338
+ "Type": "ActionOutput",
339
+ "OutputUUID": os27_screen_uuid,
340
+ "OutputName": "On Screen Content",
341
+ },
342
+ " / vpns: ",
343
+ {
344
+ "Type": "ActionOutput",
345
+ "OutputUUID": os27_find_vpns_uuid,
346
+ "OutputName": "VPNs",
347
+ },
348
+ " / current vpn: ",
349
+ {
350
+ "Type": "ActionOutput",
351
+ "OutputUUID": os27_vpn_uuid,
352
+ "OutputName": "VPN",
353
+ },
354
+ ]
355
+ )
356
+ },
357
+ },
358
+ ]
359
+ )
360
+ return extras_used
361
+
362
+
363
+ def build_case(
364
+ case_name: str,
365
+ idx: int,
366
+ rng: random.Random,
367
+ *,
368
+ include_os27_actions: bool = False,
369
+ ) -> BuildResult:
370
+ uid = lambda label: seeded_uuid(f"{case_name}-{label}")
371
+ actions: list[dict[str, Any]] = []
372
+
373
+ actions.extend(
374
+ [
375
+ {
376
+ "WFWorkflowActionIdentifier": "is.workflow.actions.comment",
377
+ "WFWorkflowActionParameters": {
378
+ "WFCommentActionText": (
379
+ f"{case_name}: random mixed-action validation stress test case."
380
+ )
381
+ },
382
+ },
383
+ {
384
+ "WFWorkflowActionIdentifier": "is.workflow.actions.comment",
385
+ "WFWorkflowActionParameters": {"WFCommentActionText": prompt_comment(case_name)},
386
+ },
387
+ {
388
+ "WFWorkflowActionIdentifier": "is.workflow.actions.comment",
389
+ "WFWorkflowActionParameters": {
390
+ "WFCommentActionText": (
391
+ "Section 1: Gather date, location, and weather data."
392
+ )
393
+ },
394
+ },
395
+ {
396
+ "WFWorkflowActionIdentifier": "is.workflow.actions.comment",
397
+ "WFWorkflowActionParameters": {
398
+ "WFCommentActionText": (
399
+ "Section 2: Fetch web data and derive numeric values."
400
+ )
401
+ },
402
+ },
403
+ {
404
+ "WFWorkflowActionIdentifier": "is.workflow.actions.comment",
405
+ "WFWorkflowActionParameters": {
406
+ "WFCommentActionText": (
407
+ "Section 3: Rename output and present a final summary."
408
+ )
409
+ },
410
+ },
411
+ ]
412
+ )
413
+
414
+ # Date + formatting
415
+ date_uuid = uid("date")
416
+ fmt_uuid = uid("format-date")
417
+ actions.append(
418
+ {
419
+ "WFWorkflowActionIdentifier": "is.workflow.actions.date",
420
+ "WFWorkflowActionParameters": {"UUID": date_uuid, "WFDateActionMode": "Current Date"},
421
+ }
422
+ )
423
+ actions.append(
424
+ {
425
+ "WFWorkflowActionIdentifier": "is.workflow.actions.format.date",
426
+ "WFWorkflowActionParameters": {
427
+ "UUID": fmt_uuid,
428
+ "WFDate": token_string_action_output(date_uuid, "Date"),
429
+ "WFDateFormatStyle": "Custom",
430
+ "WFDateFormat": "Custom",
431
+ "WFDateFormatString": rng.choice(
432
+ ["yyyy-MM-dd", "MMM d, yyyy", "yyyy-MM-dd HH:mm"]
433
+ ),
434
+ },
435
+ }
436
+ )
437
+
438
+ # Location + weather
439
+ loc_uuid = uid("current-location")
440
+ weather_uuid = uid("weather")
441
+ weather_detail_uuid = uid("weather-detail")
442
+ weather_var_uuid = uid("weather-var")
443
+ loc_var_uuid = uid("location-var")
444
+
445
+ actions.append(
446
+ {
447
+ "WFWorkflowActionIdentifier": "is.workflow.actions.getcurrentlocation",
448
+ "WFWorkflowActionParameters": {"UUID": loc_uuid},
449
+ }
450
+ )
451
+
452
+ use_location_var = rng.choice([True, False])
453
+ location_var_name = f"Location Ref {idx}"
454
+ location_output = action_output(loc_uuid, "Current Location")
455
+ if use_location_var:
456
+ actions.append(
457
+ {
458
+ "WFWorkflowActionIdentifier": "is.workflow.actions.setvariable",
459
+ "WFWorkflowActionParameters": {
460
+ "UUID": loc_var_uuid,
461
+ "WFVariableName": location_var_name,
462
+ "WFInput": location_output,
463
+ },
464
+ }
465
+ )
466
+
467
+ weather_ident = rng.choice(
468
+ [
469
+ "is.workflow.actions.weather.currentconditions",
470
+ "is.workflow.actions.weather.forecast",
471
+ ]
472
+ )
473
+ weather_params: dict[str, Any] = {
474
+ "UUID": weather_uuid,
475
+ # Keep weather location typed by wiring directly from Get Current Location.
476
+ "WFWeatherCustomLocation": location_output,
477
+ }
478
+ if weather_ident == "is.workflow.actions.weather.forecast":
479
+ weather_params["WFWeatherForecastType"] = rng.choice(["Hourly", "Daily"])
480
+ actions.append(
481
+ {
482
+ "WFWorkflowActionIdentifier": weather_ident,
483
+ "WFWorkflowActionParameters": weather_params,
484
+ }
485
+ )
486
+
487
+ use_weather_var = rng.choice([True, False])
488
+ weather_input: dict[str, Any]
489
+ if use_weather_var:
490
+ weather_var_name = f"Weather Data {idx}"
491
+ actions.append(
492
+ {
493
+ "WFWorkflowActionIdentifier": "is.workflow.actions.setvariable",
494
+ "WFWorkflowActionParameters": {
495
+ "UUID": weather_var_uuid,
496
+ "WFVariableName": weather_var_name,
497
+ "WFInput": action_output(weather_uuid, "Weather Conditions"),
498
+ },
499
+ }
500
+ )
501
+ weather_input = action_output(weather_uuid, "Weather Conditions")
502
+
503
+ actions.append(
504
+ {
505
+ "WFWorkflowActionIdentifier": "is.workflow.actions.properties.weather.conditions",
506
+ "WFWorkflowActionParameters": {
507
+ "UUID": weather_detail_uuid,
508
+ "WFInput": weather_input,
509
+ "WFContentItemPropertyName": rng.choice(WEATHER_DETAILS),
510
+ },
511
+ }
512
+ )
513
+
514
+ # Explicit Location action (tests WFLocation wiring)
515
+ location_action_uuid = uid("location-action")
516
+ actions.append(
517
+ {
518
+ "WFWorkflowActionIdentifier": "is.workflow.actions.location",
519
+ "WFWorkflowActionParameters": {
520
+ "UUID": location_action_uuid,
521
+ # Keep Location action typed by wiring directly from Get Current Location.
522
+ "WFLocation": location_output,
523
+ },
524
+ }
525
+ )
526
+
527
+ # Web data chain
528
+ url_uuid = uid("url")
529
+ dl_uuid = uid("download")
530
+ dict_uuid = uid("detect-dictionary")
531
+ key_uuid = uid("get-key")
532
+ split_uuid = uid("split")
533
+ count_uuid = uid("count")
534
+ endpoint, dict_key = rng.choice(ENDPOINTS)
535
+ endpoint = endpoint.format(i=idx)
536
+ separator = rng.choice(["/", ":", "?", "-"])
537
+
538
+ actions.append(
539
+ {
540
+ "WFWorkflowActionIdentifier": "is.workflow.actions.url",
541
+ "WFWorkflowActionParameters": {"UUID": url_uuid, "WFURLActionURL": endpoint},
542
+ }
543
+ )
544
+ actions.append(
545
+ {
546
+ "WFWorkflowActionIdentifier": "is.workflow.actions.downloadurl",
547
+ "WFWorkflowActionParameters": {
548
+ "UUID": dl_uuid,
549
+ "WFURL": token_string_action_output(url_uuid, "URL"),
550
+ "WFHTTPMethod": "GET",
551
+ },
552
+ }
553
+ )
554
+ actions.append(
555
+ {
556
+ "WFWorkflowActionIdentifier": "is.workflow.actions.detect.dictionary",
557
+ "WFWorkflowActionParameters": {
558
+ "UUID": dict_uuid,
559
+ "WFInput": action_output(dl_uuid, "Contents of URL"),
560
+ },
561
+ }
562
+ )
563
+ actions.append(
564
+ {
565
+ "WFWorkflowActionIdentifier": "is.workflow.actions.getvalueforkey",
566
+ "WFWorkflowActionParameters": {
567
+ "UUID": key_uuid,
568
+ "WFDictionaryKey": dict_key,
569
+ "WFInput": action_output(dict_uuid, "Dictionary"),
570
+ },
571
+ }
572
+ )
573
+ actions.append(
574
+ {
575
+ "WFWorkflowActionIdentifier": "is.workflow.actions.text.split",
576
+ "WFWorkflowActionParameters": {
577
+ "UUID": split_uuid,
578
+ "WFTextSeparator": "Custom",
579
+ "WFTextCustomSeparator": separator,
580
+ "text": token_string_action_output(key_uuid, "Dictionary Value"),
581
+ },
582
+ }
583
+ )
584
+ count_input = action_output(split_uuid, "Split Text")
585
+ actions.append(
586
+ {
587
+ "WFWorkflowActionIdentifier": "is.workflow.actions.count",
588
+ "WFWorkflowActionParameters": {
589
+ "UUID": count_uuid,
590
+ "WFInput": count_input,
591
+ "Input": count_input,
592
+ },
593
+ }
594
+ )
595
+
596
+ # Numeric chain
597
+ num_uuid = uid("number")
598
+ math_uuid = uid("math")
599
+ round_uuid = uid("round")
600
+ math_op = rng.choice([None, "-", "×", "÷"])
601
+ math_operand = rng.randint(2, 9)
602
+ math_params: dict[str, Any] = {
603
+ "UUID": math_uuid,
604
+ "WFMathOperand": str(math_operand),
605
+ "WFInput": action_output(num_uuid, "Number"),
606
+ }
607
+ if math_op is not None:
608
+ math_params["WFMathOperation"] = math_op
609
+ actions.append(
610
+ {
611
+ "WFWorkflowActionIdentifier": "is.workflow.actions.number",
612
+ "WFWorkflowActionParameters": {"UUID": num_uuid, "WFNumberActionNumber": rng.randint(1, 30)},
613
+ }
614
+ )
615
+ actions.append(
616
+ {
617
+ "WFWorkflowActionIdentifier": "is.workflow.actions.math",
618
+ "WFWorkflowActionParameters": math_params,
619
+ }
620
+ )
621
+ actions.append(
622
+ {
623
+ "WFWorkflowActionIdentifier": "is.workflow.actions.round",
624
+ "WFWorkflowActionParameters": {
625
+ "UUID": round_uuid,
626
+ "WFRoundMode": "Normal",
627
+ "WFInput": action_output(math_uuid, "Calculation Result"),
628
+ },
629
+ }
630
+ )
631
+
632
+ # Set Name action
633
+ rename_uuid = uid("set-name")
634
+ actions.append(
635
+ {
636
+ "WFWorkflowActionIdentifier": "is.workflow.actions.setitemname",
637
+ "WFWorkflowActionParameters": {
638
+ "UUID": rename_uuid,
639
+ "WFInput": action_output(key_uuid, "Dictionary Value"),
640
+ "WFName": f"Test-{idx:03d}.txt",
641
+ },
642
+ }
643
+ )
644
+
645
+ # Optional random extras
646
+ extras_used: list[str] = []
647
+ extra_count = rng.randint(0, 2)
648
+ for module in rng.sample(EXTRA_MODULES, extra_count):
649
+ if module == "detect.link":
650
+ extras_used.append(module)
651
+ actions.append(
652
+ {
653
+ "WFWorkflowActionIdentifier": "is.workflow.actions.detect.link",
654
+ "WFWorkflowActionParameters": {
655
+ "UUID": uid(f"detect-link-{len(extras_used)}"),
656
+ "WFInput": token_string_action_output(key_uuid, "Dictionary Value"),
657
+ },
658
+ }
659
+ )
660
+ elif module == "text.changecase":
661
+ extras_used.append(module)
662
+ actions.append(
663
+ {
664
+ "WFWorkflowActionIdentifier": "is.workflow.actions.text.changecase",
665
+ "WFWorkflowActionParameters": {
666
+ "UUID": uid(f"change-case-{len(extras_used)}"),
667
+ "WFCaseType": rng.choice(["UPPERCASE", "lowercase", "Capitalize Every Word"]),
668
+ "text": token_string_action_output(key_uuid, "Dictionary Value"),
669
+ },
670
+ }
671
+ )
672
+ elif module == "base64":
673
+ extras_used.append(module)
674
+ actions.append(
675
+ {
676
+ "WFWorkflowActionIdentifier": "is.workflow.actions.base64encode",
677
+ "WFWorkflowActionParameters": {
678
+ "UUID": uid(f"base64-{len(extras_used)}"),
679
+ "WFEncodeMode": "Encode",
680
+ "WFBase64LineBreakMode": "None",
681
+ "WFInput": action_output(key_uuid, "Dictionary Value"),
682
+ },
683
+ }
684
+ )
685
+
686
+ if include_os27_actions:
687
+ extras_used.extend(append_os27_actions(actions, case_name, idx, uid))
688
+
689
+ # Final summary
690
+ summary_uuid = uid("summary")
691
+ actions.append(
692
+ {
693
+ "WFWorkflowActionIdentifier": "is.workflow.actions.gettext",
694
+ "WFWorkflowActionParameters": {
695
+ "UUID": summary_uuid,
696
+ "WFTextActionText": token_string_from_parts(
697
+ [
698
+ f"Case {idx}: date ",
699
+ {
700
+ "Type": "ActionOutput",
701
+ "OutputUUID": fmt_uuid,
702
+ "OutputName": "Formatted Date",
703
+ },
704
+ ", weather ",
705
+ {
706
+ "Type": "ActionOutput",
707
+ "OutputUUID": weather_detail_uuid,
708
+ "OutputName": "Details of Weather Conditions",
709
+ },
710
+ ", key ",
711
+ {
712
+ "Type": "ActionOutput",
713
+ "OutputUUID": key_uuid,
714
+ "OutputName": "Dictionary Value",
715
+ },
716
+ ", count ",
717
+ {
718
+ "Type": "ActionOutput",
719
+ "OutputUUID": count_uuid,
720
+ "OutputName": "Count",
721
+ },
722
+ ", math ",
723
+ {
724
+ "Type": "ActionOutput",
725
+ "OutputUUID": round_uuid,
726
+ "OutputName": "Rounded Number",
727
+ },
728
+ ", renamed ",
729
+ {
730
+ "Type": "ActionOutput",
731
+ "OutputUUID": rename_uuid,
732
+ "OutputName": "Renamed File",
733
+ },
734
+ ".",
735
+ ]
736
+ ),
737
+ },
738
+ }
739
+ )
740
+ actions.append(
741
+ {
742
+ "WFWorkflowActionIdentifier": "is.workflow.actions.showresult",
743
+ "WFWorkflowActionParameters": {"Text": token_string_action_output(summary_uuid, "Text")},
744
+ }
745
+ )
746
+
747
+ workflow = make_workflow(case_name, actions)
748
+ action_ids = [
749
+ action.get("WFWorkflowActionIdentifier")
750
+ for action in actions
751
+ if action.get("WFWorkflowActionIdentifier") != "is.workflow.actions.comment"
752
+ ]
753
+ distinct_actions = len(set(action_ids))
754
+ return BuildResult(
755
+ workflow=workflow,
756
+ distinct_actions=distinct_actions,
757
+ action_ids=action_ids,
758
+ extras_used=extras_used,
759
+ )
760
+
761
+
762
+ def write_shortcut(path: Path, workflow: dict[str, Any]) -> None:
763
+ path.parent.mkdir(parents=True, exist_ok=True)
764
+ path.write_bytes(plistlib.dumps(workflow, fmt=plistlib.FMT_XML, sort_keys=False))
765
+
766
+
767
+ def _run_shortcuts_sign(input_path: Path, output_path: Path) -> tuple[int, str]:
768
+ proc = subprocess.run(
769
+ [
770
+ "shortcuts",
771
+ "sign",
772
+ "--mode",
773
+ "anyone",
774
+ "--input",
775
+ str(input_path),
776
+ "--output",
777
+ str(output_path),
778
+ ],
779
+ capture_output=True,
780
+ text=True,
781
+ )
782
+ output = (proc.stdout or "").strip() + ("\n" + proc.stderr.strip() if proc.stderr else "")
783
+ return proc.returncode, output.strip()
784
+
785
+
786
+ def sign_shortcut(path: Path) -> tuple[int, str]:
787
+ # Apple's signer can reject hidden dotfile inputs with a misleading
788
+ # "not in the correct format" error, so keep temporary shortcut names visible.
789
+ tmp_input = path.with_name(f"{path.stem}-sign-input-{os.getpid()}.shortcut")
790
+ tmp_output = path.with_name(f"{path.stem}-sign-output-{os.getpid()}.shortcut")
791
+ try:
792
+ shutil.copy2(path, tmp_input)
793
+ rc, output = _run_shortcuts_sign(tmp_input, tmp_output)
794
+ if rc == 0:
795
+ shutil.move(str(tmp_output), path)
796
+ return 0, output
797
+
798
+ first_output = output
799
+ shutil.copy2(path, tmp_input)
800
+ tmp_output.unlink(missing_ok=True)
801
+ lint = subprocess.run(["plutil", "-lint", str(tmp_input)], capture_output=True, text=True)
802
+ if lint.returncode != 0:
803
+ lint_output = (lint.stdout or "").strip() + (
804
+ "\n" + lint.stderr.strip() if lint.stderr else ""
805
+ )
806
+ return rc, "\n".join(part for part in (first_output, lint_output.strip()) if part)
807
+
808
+ convert = subprocess.run(
809
+ ["plutil", "-convert", "binary1", str(tmp_input)],
810
+ capture_output=True,
811
+ text=True,
812
+ )
813
+ if convert.returncode != 0:
814
+ convert_output = (convert.stdout or "").strip() + (
815
+ "\n" + convert.stderr.strip() if convert.stderr else ""
816
+ )
817
+ return rc, "\n".join(part for part in (first_output, convert_output.strip()) if part)
818
+
819
+ rc_binary, output_binary = _run_shortcuts_sign(tmp_input, tmp_output)
820
+ if rc_binary == 0:
821
+ shutil.move(str(tmp_output), path)
822
+ return 0, "\n".join(
823
+ part
824
+ for part in (
825
+ first_output,
826
+ "Retried after binary plist conversion.",
827
+ output_binary,
828
+ )
829
+ if part
830
+ )
831
+ return rc_binary, "\n".join(part for part in (first_output, output_binary) if part)
832
+ finally:
833
+ tmp_input.unlink(missing_ok=True)
834
+ tmp_output.unlink(missing_ok=True)
835
+
836
+
837
+ def main() -> int:
838
+ parser = argparse.ArgumentParser(
839
+ description="Generate random mixed-action shortcut matrix and validate with retries."
840
+ )
841
+ parser.add_argument("--count", type=int, default=50, help="Number of shortcuts to generate")
842
+ parser.add_argument("--min-actions", type=int, default=10, help="Minimum distinct actions per shortcut")
843
+ parser.add_argument("--seed", type=int, default=260224, help="Base RNG seed")
844
+ parser.add_argument("--max-attempts", type=int, default=20, help="Generation retries per shortcut")
845
+ parser.add_argument("--output-dir", help="Override output directory")
846
+ parser.add_argument("--sign", action="store_true", help="Sign generated shortcuts after validation")
847
+ parser.add_argument(
848
+ "--target-macos",
849
+ default="auto",
850
+ help="Validation target macOS major: auto, latest, 26, or 27",
851
+ )
852
+ parser.add_argument(
853
+ "--target-platform",
854
+ default="macos",
855
+ help="Validation target platform: macos, ios, ipad, iphone, or all",
856
+ )
857
+ parser.add_argument(
858
+ "--include-os27-actions",
859
+ action="store_true",
860
+ help="Include an explicit OS 27 action module in every generated shortcut",
861
+ )
862
+ args = parser.parse_args()
863
+ target_macos_major = resolve_target_macos_major(args.target_macos)
864
+ target_platform = resolve_target_platform(args.target_platform)
865
+ if args.include_os27_actions and target_macos_major not in (27, None):
866
+ parser.error("--include-os27-actions requires --target-macos 27 or latest")
867
+
868
+ runid = dt.datetime.now().strftime("%H%M%S")
869
+ date_folder = dt.datetime.now().strftime("%Y-%m-%d")
870
+ configured_output_dir = (
871
+ args.output_dir
872
+ or os.environ.get("SHORTCUTS_PLAYGROUND_OUTPUT_DIR")
873
+ or os.environ.get("CLAUDE_PLUGIN_OPTION_OUTPUT_DIR")
874
+ or str(Path.home() / "Documents" / "Shortcuts Playground")
875
+ )
876
+ out_dir = (
877
+ Path(configured_output_dir).expanduser().resolve()
878
+ / date_folder
879
+ / f"random-mixed-actions-{runid}"
880
+ )
881
+ out_dir.mkdir(parents=True, exist_ok=True)
882
+
883
+ skill_dir = Path(__file__).resolve().parents[1]
884
+ allowed_ids = load_allowed_ids(skill_dir, target_macos_major, target_platform)
885
+ allowed_glyph_ids, allowed_icon_colors = load_icon_metadata(skill_dir)
886
+ unavailable_ids = load_future_toolkit_id_reasons(
887
+ skill_dir,
888
+ target_macos_major,
889
+ target_platform,
890
+ )
891
+ unavailable_parameter_keys = load_future_parameter_key_reasons(target_macos_major)
892
+ toolkit_parameter_schemas = load_toolkit_parameter_schemas(
893
+ skill_dir,
894
+ target_macos_major,
895
+ target_platform,
896
+ )
897
+ toolkit_parameter_enum_cases = load_toolkit_parameter_enum_cases(
898
+ skill_dir,
899
+ target_macos_major,
900
+ target_platform,
901
+ )
902
+ toolkit_parameter_boolean_keys = load_toolkit_parameter_boolean_keys(
903
+ skill_dir,
904
+ target_macos_major,
905
+ target_platform,
906
+ )
907
+
908
+ results: list[dict[str, Any]] = []
909
+ manifest: list[dict[str, Any]] = []
910
+ action_coverage: dict[str, int] = {}
911
+ distinct_counts: list[int] = []
912
+ failed_cases = 0
913
+
914
+ for idx in range(1, args.count + 1):
915
+ case_prefix = "ZZ-RAND27" if args.include_os27_actions else "ZZ-RAND26"
916
+ case_name = f"{case_prefix}-{runid}-{idx:03d}"
917
+ case_path = out_dir / f"{case_name}.shortcut"
918
+ case_record: dict[str, Any] = {
919
+ "index": idx,
920
+ "name": case_name,
921
+ "path": str(case_path),
922
+ "attempts": [],
923
+ "status": "failed",
924
+ }
925
+
926
+ passed: Optional[BuildResult] = None
927
+ for attempt in range(1, args.max_attempts + 1):
928
+ rng = random.Random(args.seed + idx * 1000 + attempt)
929
+ built = build_case(
930
+ case_name,
931
+ idx,
932
+ rng,
933
+ include_os27_actions=args.include_os27_actions,
934
+ )
935
+
936
+ if built.distinct_actions < args.min_actions:
937
+ case_record["attempts"].append(
938
+ {
939
+ "attempt": attempt,
940
+ "distinct_actions": built.distinct_actions,
941
+ "valid": False,
942
+ "errors": [f"distinct action count below {args.min_actions}"],
943
+ "extras_used": built.extras_used,
944
+ }
945
+ )
946
+ continue
947
+
948
+ errors, _ = validate(
949
+ built.workflow,
950
+ allowed_ids,
951
+ allowed_glyph_ids,
952
+ allowed_icon_colors,
953
+ unavailable_ids=unavailable_ids,
954
+ unavailable_parameter_keys=unavailable_parameter_keys,
955
+ toolkit_parameter_schemas=toolkit_parameter_schemas,
956
+ toolkit_parameter_enum_cases=toolkit_parameter_enum_cases,
957
+ toolkit_parameter_boolean_keys=toolkit_parameter_boolean_keys,
958
+ )
959
+ case_record["attempts"].append(
960
+ {
961
+ "attempt": attempt,
962
+ "distinct_actions": built.distinct_actions,
963
+ "valid": len(errors) == 0,
964
+ "errors": errors[:20],
965
+ "extras_used": built.extras_used,
966
+ }
967
+ )
968
+ if not errors:
969
+ passed = built
970
+ break
971
+
972
+ if passed is None:
973
+ failed_cases += 1
974
+ results.append(case_record)
975
+ continue
976
+
977
+ write_shortcut(case_path, passed.workflow)
978
+ sign_info: dict[str, Any] = {"skipped": True}
979
+ if args.sign:
980
+ rc, output = sign_shortcut(case_path)
981
+ sign_info = {"skipped": False, "rc": rc, "output_tail": output[-1200:]}
982
+ if rc != 0:
983
+ case_record["status"] = "failed-sign"
984
+ case_record["sign"] = sign_info
985
+ results.append(case_record)
986
+ failed_cases += 1
987
+ continue
988
+
989
+ distinct_counts.append(passed.distinct_actions)
990
+ for ident in set(passed.action_ids):
991
+ action_coverage[ident] = action_coverage.get(ident, 0) + 1
992
+
993
+ manifest.append(
994
+ {
995
+ "name": case_name,
996
+ "path": str(case_path),
997
+ "distinct_actions": passed.distinct_actions,
998
+ "action_ids": passed.action_ids,
999
+ "extras_used": passed.extras_used,
1000
+ }
1001
+ )
1002
+ case_record["status"] = "passed"
1003
+ case_record["distinct_actions"] = passed.distinct_actions
1004
+ case_record["extras_used"] = passed.extras_used
1005
+ case_record["sign"] = sign_info
1006
+ results.append(case_record)
1007
+
1008
+ (out_dir / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
1009
+ (out_dir / "results.json").write_text(json.dumps(results, indent=2), encoding="utf-8")
1010
+
1011
+ passed_cases = len([r for r in results if r.get("status") == "passed"])
1012
+ total_attempts = sum(len(r.get("attempts", [])) for r in results)
1013
+ retried_cases = len([r for r in results if len(r.get("attempts", [])) > 1])
1014
+
1015
+ summary_lines = [
1016
+ f"# Random Mixed Action Stress Test ({runid})",
1017
+ "",
1018
+ f"- Date: {dt.datetime.now().isoformat(timespec='seconds')}",
1019
+ f"- Requested cases: {args.count}",
1020
+ f"- Passed cases: {passed_cases}",
1021
+ f"- Failed cases: {failed_cases}",
1022
+ f"- Minimum distinct actions target: {args.min_actions}",
1023
+ f"- Target macOS: {args.target_macos}",
1024
+ f"- Target platform: {args.target_platform}",
1025
+ f"- OS 27 action module: {'enabled' if args.include_os27_actions else 'disabled'}",
1026
+ f"- Total generation attempts: {total_attempts}",
1027
+ f"- Cases requiring retries: {retried_cases}",
1028
+ ]
1029
+
1030
+ if distinct_counts:
1031
+ summary_lines.extend(
1032
+ [
1033
+ f"- Distinct actions per passed shortcut (min/max): {min(distinct_counts)}/{max(distinct_counts)}",
1034
+ f"- Distinct actions average: {sum(distinct_counts) / len(distinct_counts):.2f}",
1035
+ ]
1036
+ )
1037
+
1038
+ summary_lines.extend(["", "## Action Coverage (passed cases)"])
1039
+ for ident in sorted(action_coverage):
1040
+ summary_lines.append(f"- {ident}: {action_coverage[ident]}")
1041
+
1042
+ summary_lines.extend(
1043
+ [
1044
+ "",
1045
+ "## Artifacts",
1046
+ f"- manifest.json: {out_dir / 'manifest.json'}",
1047
+ f"- results.json: {out_dir / 'results.json'}",
1048
+ ]
1049
+ )
1050
+
1051
+ (out_dir / "summary.md").write_text("\n".join(summary_lines) + "\n", encoding="utf-8")
1052
+
1053
+ print(f"OUT_DIR={out_dir}")
1054
+ print(
1055
+ f"TOTAL={args.count} PASSED={passed_cases} FAILED={failed_cases} "
1056
+ f"RETRIED={retried_cases} ATTEMPTS={total_attempts}"
1057
+ )
1058
+ if distinct_counts:
1059
+ print(
1060
+ f"DISTINCT_ACTIONS_MIN={min(distinct_counts)} "
1061
+ f"DISTINCT_ACTIONS_MAX={max(distinct_counts)}"
1062
+ )
1063
+ return 0 if failed_cases == 0 else 1
1064
+
1065
+
1066
+ if __name__ == "__main__":
1067
+ raise SystemExit(main())