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,1164 @@
1
+ #!/usr/bin/env python3
2
+ """Look up reviewed Apple-derived Shortcuts grounding metadata.
3
+
4
+ This helper reads the packaged static macOS 27 Shortpy catalog plus compact
5
+ ToolKit v78 first-party parameter-key, enum-case, trigger, and exported
6
+ workflow-trigger snapshots. It never reads the user's live Shortcuts databases
7
+ and never calls private Apple frameworks.
8
+ Use it as an authoring aid when a macOS 27 action or Apple Shortpy function
9
+ name needs additional grounding beyond the markdown references.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import platform
18
+ import subprocess
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+
23
+ TARGET_MACOS_ENV_VARS = (
24
+ "SHORTCUTS_PLAYGROUND_TARGET_MACOS",
25
+ "CLAUDE_PLUGIN_OPTION_TARGET_MACOS",
26
+ )
27
+ TARGET_PLATFORM_ENV_VARS = (
28
+ "SHORTCUTS_PLAYGROUND_TARGET_PLATFORM",
29
+ "CLAUDE_PLUGIN_OPTION_TARGET_PLATFORM",
30
+ )
31
+ TOOLKIT_SNAPSHOT_MIN_MACOS_MAJOR = {
32
+ "toolkit-v78": 27,
33
+ "toolkit-v78-ios27": 27,
34
+ }
35
+ PARAMETER_CATALOG_MIN_MACOS_MAJOR = 27
36
+ TRIGGER_CATALOG_MIN_MACOS_MAJOR = 27
37
+ WORKFLOW_TRIGGER_CATALOG_MIN_MACOS_MAJOR = 27
38
+ DEPRECATED_IDENTIFIER_NOTES = {
39
+ "is.workflow.actions.getonscreencontent": (
40
+ "Deprecated in ToolKit v78; use is.workflow.actions.getonscreencontext "
41
+ "for new Get What's On Screen shortcuts."
42
+ ),
43
+ }
44
+
45
+
46
+ def toolkit_snapshot_min_macos_major(version: str | None) -> int | None:
47
+ if not isinstance(version, str) or not version:
48
+ return None
49
+ if version in TOOLKIT_SNAPSHOT_MIN_MACOS_MAJOR:
50
+ return TOOLKIT_SNAPSHOT_MIN_MACOS_MAJOR[version]
51
+ import re
52
+
53
+ match = re.search(r"v(\d+)", version)
54
+ if match and int(match.group(1)) >= 78:
55
+ return 27
56
+ return None
57
+
58
+
59
+ def skill_dir() -> Path:
60
+ return Path(__file__).resolve().parents[1]
61
+
62
+
63
+ def catalog_path(base: Path | None = None) -> Path:
64
+ return (base or skill_dir()) / "data/macos27-shortpy-grounding.json"
65
+
66
+
67
+ def parameter_catalog_path(base: Path | None = None) -> Path:
68
+ return (base or skill_dir()) / "data/toolkit-v78-first-party-parameter-keys.json"
69
+
70
+
71
+ def enum_catalog_path(base: Path | None = None) -> Path:
72
+ return (base or skill_dir()) / "data/toolkit-v78-first-party-enum-cases.json"
73
+
74
+
75
+ def trigger_catalog_path(base: Path | None = None) -> Path:
76
+ return (base or skill_dir()) / "data/toolkit-v78-trigger-parameter-keys.json"
77
+
78
+
79
+ def workflow_trigger_catalog_path(base: Path | None = None) -> Path:
80
+ return (base or skill_dir()) / "data/macos27-workflow-trigger-samples.json"
81
+
82
+
83
+ def load_catalog(base: Path | None = None) -> dict[str, Any]:
84
+ path = catalog_path(base)
85
+ with path.open("r", encoding="utf-8") as handle:
86
+ return json.load(handle)
87
+
88
+
89
+ def load_parameter_catalog(base: Path | None = None) -> dict[str, Any]:
90
+ path = parameter_catalog_path(base)
91
+ if not path.exists():
92
+ return {"tools": {}}
93
+ with path.open("r", encoding="utf-8") as handle:
94
+ return json.load(handle)
95
+
96
+
97
+ def load_enum_catalog(base: Path | None = None) -> dict[str, Any]:
98
+ path = enum_catalog_path(base)
99
+ if not path.exists():
100
+ return {"types": {}}
101
+ with path.open("r", encoding="utf-8") as handle:
102
+ return json.load(handle)
103
+
104
+
105
+ def load_trigger_catalog(base: Path | None = None) -> dict[str, Any]:
106
+ path = trigger_catalog_path(base)
107
+ if not path.exists():
108
+ return {"triggers": {}}
109
+ with path.open("r", encoding="utf-8") as handle:
110
+ return json.load(handle)
111
+
112
+
113
+ def load_workflow_trigger_catalog(base: Path | None = None) -> dict[str, Any]:
114
+ path = workflow_trigger_catalog_path(base)
115
+ if not path.exists():
116
+ return {"triggers": {}}
117
+ with path.open("r", encoding="utf-8") as handle:
118
+ return json.load(handle)
119
+
120
+
121
+ def load_identifier_min_macos(base: Path | None = None) -> dict[str, int | None]:
122
+ """Return the earliest packaged snapshot availability for each identifier."""
123
+
124
+ data_dir = (base or skill_dir()) / "data"
125
+ availability: dict[str, int | None] = {}
126
+ for path in sorted(data_dir.glob("toolkit-v*-tool-ids.json")):
127
+ with path.open("r", encoding="utf-8") as handle:
128
+ payload = json.load(handle)
129
+ version = payload.get("version") or path.stem.replace("-tool-ids", "")
130
+ min_macos = toolkit_snapshot_min_macos_major(version)
131
+ ids = set(payload.get("ids") or [])
132
+ ids.update(payload.get("control_flow_exceptions_missing_from_tools_table") or [])
133
+ for identifier in ids:
134
+ if identifier not in availability:
135
+ availability[identifier] = min_macos
136
+ elif availability[identifier] is not None and (
137
+ min_macos is None or min_macos < availability[identifier]
138
+ ):
139
+ availability[identifier] = min_macos
140
+ return availability
141
+
142
+
143
+ def host_macos_major() -> int | None:
144
+ if platform.system() != "Darwin":
145
+ return None
146
+ try:
147
+ result = subprocess.run(
148
+ ["sw_vers", "-productVersion"],
149
+ capture_output=True,
150
+ text=True,
151
+ timeout=2,
152
+ check=False,
153
+ )
154
+ except (OSError, subprocess.SubprocessError):
155
+ return None
156
+ if result.returncode != 0:
157
+ return None
158
+ major = result.stdout.strip().split(".", 1)[0]
159
+ return int(major) if major.isdigit() else None
160
+
161
+
162
+ def resolve_target_macos(value: str | None) -> int | None:
163
+ raw = value
164
+ if raw is None:
165
+ for env_name in TARGET_MACOS_ENV_VARS:
166
+ raw = os.environ.get(env_name)
167
+ if raw:
168
+ break
169
+ if raw is None or raw == "" or raw.lower() == "auto":
170
+ detected = host_macos_major()
171
+ return detected if detected is not None else 26
172
+ if raw.lower() in {"latest", "all"}:
173
+ return None
174
+ if raw.isdigit():
175
+ return int(raw)
176
+ raise ValueError(f"Invalid macOS target: {raw!r}")
177
+
178
+
179
+ def resolve_target_platform(value: str | None) -> str | None:
180
+ raw = value
181
+ if raw is None:
182
+ for env_name in TARGET_PLATFORM_ENV_VARS:
183
+ raw = os.environ.get(env_name)
184
+ if raw:
185
+ break
186
+ if raw is None:
187
+ raw = "macos"
188
+ normalized = raw.strip().lower().replace("_", "-")
189
+ if normalized in {"", "auto", "host", "mac", "macos", "mac-os"}:
190
+ return "macos"
191
+ if normalized in {"ios", "ipados", "iphone", "ipad"}:
192
+ return "ios"
193
+ if normalized in {"latest", "all", "any"}:
194
+ return None
195
+ raise ValueError(f"Invalid target platform: {raw!r}")
196
+
197
+
198
+ def normalized_identifier(value: str) -> str:
199
+ if value.startswith("is.workflow.actions."):
200
+ return value
201
+ return f"is.workflow.actions.{value}"
202
+
203
+
204
+ def find_by_identifier(catalog: dict[str, Any], value: str) -> tuple[str, dict[str, Any]] | None:
205
+ tools = catalog.get("tools") or {}
206
+ structural = catalog.get("structuralActions") or {}
207
+ candidates = {**tools, **structural}
208
+ for key in (value, normalized_identifier(value)):
209
+ if key in candidates:
210
+ return key, candidates[key]
211
+ suffix = value.removeprefix("is.workflow.actions.")
212
+ matches = [
213
+ (identifier, entry)
214
+ for identifier, entry in candidates.items()
215
+ if identifier.endswith(f".{suffix}") or identifier.rsplit(".", 1)[-1] == suffix
216
+ ]
217
+ return matches[0] if len(matches) == 1 else None
218
+
219
+
220
+ def find_by_python_name(catalog: dict[str, Any], value: str) -> tuple[str, dict[str, Any]] | None:
221
+ lookup = catalog.get("pythonLookup") or {}
222
+ if value in lookup:
223
+ entry = lookup[value]
224
+ identifier = entry.get("wfIdentifier")
225
+ if identifier:
226
+ found = find_by_identifier(catalog, identifier)
227
+ return found or (identifier, entry)
228
+ for identifier, entry in (catalog.get("tools") or {}).items():
229
+ if entry.get("pythonName") == value:
230
+ return identifier, entry
231
+ return None
232
+
233
+
234
+ def parameter_entry_to_grounding(identifier: str, entry: dict[str, Any]) -> dict[str, Any]:
235
+ return {
236
+ "status": "toolkit-parameter-summary",
237
+ "name": entry.get("displayName") or identifier,
238
+ "pythonName": entry.get("pythonName"),
239
+ "summary": (
240
+ "ToolKit v78 parameter-key summary. This proves the action exists "
241
+ "and names its parameters, but it is not a full authored shortcut sample."
242
+ ),
243
+ "toolkitPlatforms": entry.get("platforms") or [],
244
+ "toolkitToolType": entry.get("toolType"),
245
+ "toolkitParameterSummary": {
246
+ "parameterCount": entry.get("parameterCount"),
247
+ "parameters": entry.get("parameters") or [],
248
+ },
249
+ "parameters": [],
250
+ "sourceFunctions": {},
251
+ "sampleShortcuts": [],
252
+ }
253
+
254
+
255
+ def trigger_entry_to_grounding(identifier: str, entry: dict[str, Any]) -> dict[str, Any]:
256
+ return {
257
+ "status": "toolkit-trigger-summary",
258
+ "minimumMacOSMajor": TRIGGER_CATALOG_MIN_MACOS_MAJOR,
259
+ "name": entry.get("displayName") or identifier,
260
+ "pythonName": entry.get("pythonName"),
261
+ "summary": (
262
+ "ToolKit v78 automation-trigger metadata. This proves the trigger "
263
+ "exists and names its parameters. When present, workflowTriggerSample "
264
+ "shows the exported WFWorkflowTriggers plist shape for OS 27 shortcut XML."
265
+ ),
266
+ "toolkitPlatforms": entry.get("platforms") or [],
267
+ "toolkitTriggerSummary": {
268
+ "parameterCount": entry.get("parameterCount"),
269
+ "parameters": entry.get("parameters") or [],
270
+ "outputTypeIdentifiers": entry.get("outputTypeIdentifiers") or [],
271
+ },
272
+ "parameters": [],
273
+ "sourceFunctions": {},
274
+ "sampleShortcuts": [],
275
+ }
276
+
277
+
278
+ def augment_with_parameter_summary(
279
+ identifier: str,
280
+ entry: dict[str, Any],
281
+ parameter_catalog: dict[str, Any],
282
+ ) -> dict[str, Any]:
283
+ toolkit_entry = (parameter_catalog.get("tools") or {}).get(identifier)
284
+ if not isinstance(toolkit_entry, dict):
285
+ return entry
286
+ out = dict(entry)
287
+ out["toolkitPlatforms"] = toolkit_entry.get("platforms") or []
288
+ out["toolkitToolType"] = toolkit_entry.get("toolType")
289
+ out["toolkitParameterSummary"] = {
290
+ "parameterCount": toolkit_entry.get("parameterCount"),
291
+ "parameters": toolkit_entry.get("parameters") or [],
292
+ }
293
+ return out
294
+
295
+
296
+ def toolkit_parameter_type_names(parameter: dict[str, Any]) -> list[str]:
297
+ type_names = parameter.get("typePythonNames")
298
+ if isinstance(type_names, list):
299
+ return [str(name) for name in type_names if name]
300
+ single = parameter.get("typePythonName")
301
+ return [str(single)] if single else []
302
+
303
+
304
+ def toolkit_parameter_matches_target(
305
+ parameter: dict[str, Any],
306
+ target_platform: str | None,
307
+ ) -> bool:
308
+ platforms = parameter.get("platforms")
309
+ if not isinstance(platforms, list) or not platforms:
310
+ return True
311
+ return platform_matches_target(
312
+ [str(platform) for platform in platforms if platform],
313
+ target_platform,
314
+ )
315
+
316
+
317
+ def toolkit_parameter_type_names_for_target(
318
+ parameter: dict[str, Any],
319
+ target_platform: str | None,
320
+ ) -> list[str]:
321
+ if target_platform is not None:
322
+ by_platform = parameter.get("typePythonNamesByPlatform")
323
+ if isinstance(by_platform, dict):
324
+ names: list[str] = []
325
+ for platform, type_names in by_platform.items():
326
+ if not isinstance(platform, str) or not platform_matches_target(
327
+ [platform],
328
+ target_platform,
329
+ ):
330
+ continue
331
+ if isinstance(type_names, list):
332
+ names.extend(str(name) for name in type_names if name)
333
+ if names:
334
+ return sorted(set(names))
335
+ return toolkit_parameter_type_names(parameter)
336
+
337
+
338
+ def filter_toolkit_parameter_for_target(
339
+ parameter: dict[str, Any],
340
+ target_platform: str | None,
341
+ ) -> dict[str, Any] | None:
342
+ if not toolkit_parameter_matches_target(parameter, target_platform):
343
+ return None
344
+ out = dict(parameter)
345
+ if target_platform is not None:
346
+ type_names = toolkit_parameter_type_names_for_target(parameter, target_platform)
347
+ if len(type_names) == 1:
348
+ out["typePythonName"] = type_names[0]
349
+ out.pop("typePythonNames", None)
350
+ elif type_names:
351
+ out.pop("typePythonName", None)
352
+ out["typePythonNames"] = type_names
353
+ return out
354
+
355
+
356
+ def filter_toolkit_parameter_summary_for_target(
357
+ summary: dict[str, Any],
358
+ target_platform: str | None,
359
+ ) -> dict[str, Any]:
360
+ parameters = summary.get("parameters")
361
+ if not isinstance(parameters, list):
362
+ return summary
363
+ filtered: list[dict[str, Any]] = []
364
+ for parameter in parameters:
365
+ if not isinstance(parameter, dict):
366
+ continue
367
+ filtered_parameter = filter_toolkit_parameter_for_target(
368
+ parameter,
369
+ target_platform,
370
+ )
371
+ if filtered_parameter is not None:
372
+ filtered.append(filtered_parameter)
373
+ out = dict(summary)
374
+ out["parameters"] = filtered
375
+ out["parameterCount"] = len(filtered)
376
+ return out
377
+
378
+
379
+ def filter_entry_for_target_platform(
380
+ entry: dict[str, Any],
381
+ target_platform: str | None,
382
+ ) -> dict[str, Any]:
383
+ summary = entry.get("toolkitParameterSummary")
384
+ if not isinstance(summary, dict):
385
+ return entry
386
+ out = dict(entry)
387
+ out["toolkitParameterSummary"] = filter_toolkit_parameter_summary_for_target(
388
+ summary,
389
+ target_platform,
390
+ )
391
+ return out
392
+
393
+
394
+ def enum_type_summary(
395
+ type_python_name: str,
396
+ enum_catalog: dict[str, Any],
397
+ ) -> dict[str, Any] | None:
398
+ enum_entry = (enum_catalog.get("types") or {}).get(type_python_name)
399
+ if not isinstance(enum_entry, dict):
400
+ return None
401
+ cases = enum_entry.get("cases") or []
402
+ if not cases:
403
+ return None
404
+ return {
405
+ "typePythonName": type_python_name,
406
+ "displayNames": enum_entry.get("displayNames") or [],
407
+ "platforms": enum_entry.get("platforms") or [],
408
+ "caseCount": enum_entry.get("caseCount") or len(cases),
409
+ "cases": cases,
410
+ }
411
+
412
+
413
+ def augment_parameter_with_enum_cases(
414
+ parameter: dict[str, Any],
415
+ enum_catalog: dict[str, Any],
416
+ ) -> dict[str, Any]:
417
+ enum_types = [
418
+ enum_type
419
+ for type_name in toolkit_parameter_type_names(parameter)
420
+ if (enum_type := enum_type_summary(type_name, enum_catalog)) is not None
421
+ ]
422
+ if not enum_types:
423
+ return parameter
424
+ out = dict(parameter)
425
+ out["enumTypes"] = enum_types
426
+ if len(enum_types) == 1:
427
+ out["enumCases"] = enum_types[0]["cases"]
428
+ out["enumCaseCount"] = enum_types[0]["caseCount"]
429
+ out["enumDisplayNames"] = enum_types[0]["displayNames"]
430
+ return out
431
+
432
+
433
+ def augment_summary_parameters_with_enum_cases(
434
+ summary: dict[str, Any],
435
+ enum_catalog: dict[str, Any],
436
+ ) -> dict[str, Any]:
437
+ parameters = summary.get("parameters")
438
+ if not isinstance(parameters, list):
439
+ return summary
440
+ out = dict(summary)
441
+ out["parameters"] = [
442
+ augment_parameter_with_enum_cases(parameter, enum_catalog)
443
+ if isinstance(parameter, dict)
444
+ else parameter
445
+ for parameter in parameters
446
+ ]
447
+ return out
448
+
449
+
450
+ def augment_with_enum_cases(
451
+ entry: dict[str, Any],
452
+ enum_catalog: dict[str, Any],
453
+ ) -> dict[str, Any]:
454
+ out = dict(entry)
455
+ parameter_summary = out.get("toolkitParameterSummary")
456
+ if isinstance(parameter_summary, dict):
457
+ out["toolkitParameterSummary"] = augment_summary_parameters_with_enum_cases(
458
+ parameter_summary,
459
+ enum_catalog,
460
+ )
461
+ trigger_summary = out.get("toolkitTriggerSummary")
462
+ if isinstance(trigger_summary, dict):
463
+ out["toolkitTriggerSummary"] = augment_summary_parameters_with_enum_cases(
464
+ trigger_summary,
465
+ enum_catalog,
466
+ )
467
+ return out
468
+
469
+
470
+ def augment_with_workflow_trigger_sample(
471
+ identifier: str,
472
+ entry: dict[str, Any],
473
+ workflow_trigger_catalog: dict[str, Any],
474
+ ) -> dict[str, Any]:
475
+ sample = (workflow_trigger_catalog.get("triggers") or {}).get(identifier)
476
+ if not isinstance(sample, dict):
477
+ return entry
478
+ out = dict(entry)
479
+ out["workflowTriggerSample"] = sample
480
+ return out
481
+
482
+
483
+ def find_parameter_by_identifier(
484
+ parameter_catalog: dict[str, Any],
485
+ value: str,
486
+ ) -> tuple[str, dict[str, Any]] | None:
487
+ tools = parameter_catalog.get("tools") or {}
488
+ for key in (value, normalized_identifier(value)):
489
+ entry = tools.get(key)
490
+ if isinstance(entry, dict):
491
+ return key, parameter_entry_to_grounding(key, entry)
492
+ suffix = value.removeprefix("is.workflow.actions.")
493
+ matches = [
494
+ (identifier, parameter_entry_to_grounding(identifier, entry))
495
+ for identifier, entry in tools.items()
496
+ if identifier.endswith(f".{suffix}") or identifier.rsplit(".", 1)[-1] == suffix
497
+ ]
498
+ return matches[0] if len(matches) == 1 else None
499
+
500
+
501
+ def find_parameter_by_python_name(
502
+ parameter_catalog: dict[str, Any],
503
+ value: str,
504
+ ) -> tuple[str, dict[str, Any]] | None:
505
+ tools = parameter_catalog.get("tools") or {}
506
+ matches = [
507
+ (identifier, parameter_entry_to_grounding(identifier, entry))
508
+ for identifier, entry in tools.items()
509
+ if entry.get("pythonName") == value
510
+ ]
511
+ return matches[0] if len(matches) == 1 else None
512
+
513
+
514
+ def find_trigger_by_identifier(
515
+ trigger_catalog: dict[str, Any],
516
+ value: str,
517
+ ) -> tuple[str, dict[str, Any]] | None:
518
+ triggers = trigger_catalog.get("triggers") or {}
519
+ entry = triggers.get(value)
520
+ if isinstance(entry, dict):
521
+ return value, trigger_entry_to_grounding(value, entry)
522
+ suffix = value.removeprefix("com.apple.shortcuts.")
523
+ matches = [
524
+ (identifier, trigger_entry_to_grounding(identifier, entry))
525
+ for identifier, entry in triggers.items()
526
+ if identifier.endswith(suffix) or identifier.rsplit(".", 1)[-1] == suffix
527
+ ]
528
+ return matches[0] if len(matches) == 1 else None
529
+
530
+
531
+ def find_trigger_by_python_name(
532
+ trigger_catalog: dict[str, Any],
533
+ value: str,
534
+ ) -> tuple[str, dict[str, Any]] | None:
535
+ triggers = trigger_catalog.get("triggers") or {}
536
+ matches = [
537
+ (identifier, trigger_entry_to_grounding(identifier, entry))
538
+ for identifier, entry in triggers.items()
539
+ if entry.get("pythonName") == value
540
+ ]
541
+ return matches[0] if len(matches) == 1 else None
542
+
543
+
544
+ def entry_search_text(identifier: str, entry: dict[str, Any]) -> str:
545
+ parts: list[str] = [
546
+ identifier,
547
+ entry.get("status") or "",
548
+ entry.get("name") or "",
549
+ entry.get("pythonName") or "",
550
+ entry.get("summary") or "",
551
+ " ".join(entry.get("keywords") or []),
552
+ " ".join(entry.get("sourceFunctions") or {}),
553
+ ]
554
+ for parameter in entry.get("parameters") or []:
555
+ parts.extend(
556
+ [
557
+ parameter.get("wfKey") or "",
558
+ parameter.get("pythonKeyword") or "",
559
+ parameter.get("label") or "",
560
+ parameter.get("description") or "",
561
+ ]
562
+ )
563
+ toolkit_summary = entry.get("toolkitParameterSummary") or {}
564
+ if isinstance(toolkit_summary, dict):
565
+ for parameter in toolkit_summary.get("parameters") or []:
566
+ if isinstance(parameter, dict):
567
+ parts.extend(
568
+ [
569
+ parameter.get("key") or "",
570
+ parameter.get("name") or "",
571
+ parameter.get("typePythonName") or "",
572
+ " ".join(toolkit_parameter_type_names(parameter)),
573
+ ]
574
+ )
575
+ trigger_summary = entry.get("toolkitTriggerSummary") or {}
576
+ if isinstance(trigger_summary, dict):
577
+ parts.append(" ".join(trigger_summary.get("outputTypeIdentifiers") or []))
578
+ for parameter in trigger_summary.get("parameters") or []:
579
+ if isinstance(parameter, dict):
580
+ parts.extend(
581
+ [
582
+ parameter.get("key") or "",
583
+ parameter.get("name") or "",
584
+ parameter.get("typePythonName") or "",
585
+ " ".join(toolkit_parameter_type_names(parameter)),
586
+ ]
587
+ )
588
+ workflow_trigger = entry.get("workflowTriggerSample") or {}
589
+ if isinstance(workflow_trigger, dict):
590
+ parts.append("workflow triggers exported automation header")
591
+ parts.extend(workflow_trigger.get("notes") or [])
592
+ payload = workflow_trigger.get("workflowTrigger") or {}
593
+ if isinstance(payload, dict):
594
+ parts.append(str(payload.get("WFTriggerIdentifier") or ""))
595
+ serialized = payload.get("WFTriggerSerializedParameters") or {}
596
+ if isinstance(serialized, dict):
597
+ parts.extend(str(key) for key in serialized)
598
+ return "\n".join(parts).lower()
599
+
600
+
601
+ def search_entries(catalog: dict[str, Any], query: str, limit: int) -> list[tuple[str, dict[str, Any]]]:
602
+ terms = [term.lower() for term in query.split() if term]
603
+ tools = catalog.get("tools") or {}
604
+ structural = catalog.get("structuralActions") or {}
605
+ entries = sorted({**tools, **structural}.items())
606
+ if not terms:
607
+ return entries[:limit]
608
+ matches = [
609
+ (identifier, entry)
610
+ for identifier, entry in entries
611
+ if all(term in entry_search_text(identifier, entry) for term in terms)
612
+ ]
613
+ return matches[:limit]
614
+
615
+
616
+ def search_parameter_entries(
617
+ parameter_catalog: dict[str, Any],
618
+ query: str,
619
+ limit: int,
620
+ ) -> list[tuple[str, dict[str, Any]]]:
621
+ terms = [term.lower() for term in query.split() if term]
622
+ entries = sorted((parameter_catalog.get("tools") or {}).items())
623
+ converted = [
624
+ (identifier, parameter_entry_to_grounding(identifier, entry))
625
+ for identifier, entry in entries
626
+ ]
627
+ if not terms:
628
+ return converted[:limit]
629
+ return [
630
+ (identifier, entry)
631
+ for identifier, entry in converted
632
+ if all(term in entry_search_text(identifier, entry) for term in terms)
633
+ ][:limit]
634
+
635
+
636
+ def search_trigger_entries(
637
+ trigger_catalog: dict[str, Any],
638
+ query: str,
639
+ limit: int,
640
+ ) -> list[tuple[str, dict[str, Any]]]:
641
+ terms = [term.lower() for term in query.split() if term]
642
+ entries = sorted((trigger_catalog.get("triggers") or {}).items())
643
+ converted = [
644
+ (identifier, trigger_entry_to_grounding(identifier, entry))
645
+ for identifier, entry in entries
646
+ ]
647
+ if not terms:
648
+ return converted[:limit]
649
+ return [
650
+ (identifier, entry)
651
+ for identifier, entry in converted
652
+ if all(term in entry_search_text(identifier, entry) for term in terms)
653
+ ][:limit]
654
+
655
+
656
+ def target_note(minimum: int | None, target_macos: int | None) -> str | None:
657
+ if target_macos is not None and minimum is not None and target_macos < minimum:
658
+ return f"Requires macOS {minimum}+; target macOS is {target_macos}."
659
+ return None
660
+
661
+
662
+ def deprecation_note(identifier: str) -> str | None:
663
+ return DEPRECATED_IDENTIFIER_NOTES.get(identifier)
664
+
665
+
666
+ def platform_availability_note(entry: dict[str, Any]) -> str | None:
667
+ platforms = entry.get("toolkitPlatforms") or []
668
+ if not isinstance(platforms, list) or not platforms:
669
+ return None
670
+ platform_text = [str(platform) for platform in platforms]
671
+ has_macos = any("macos" in platform.lower() for platform in platform_text)
672
+ has_ios = any("ios" in platform.lower() for platform in platform_text)
673
+ if has_ios and not has_macos:
674
+ return "Only observed in iOS 27 Simulator ToolKit; no macOS ToolKit row observed."
675
+ if has_macos and not has_ios:
676
+ return "Only observed in macOS 27 ToolKit; no iOS 27 Simulator ToolKit row observed."
677
+ return None
678
+
679
+
680
+ def target_platform_label(target_platform: str | None) -> str:
681
+ if target_platform == "ios":
682
+ return "iOS/iPadOS"
683
+ if target_platform == "macos":
684
+ return "macOS"
685
+ return "all platforms"
686
+
687
+
688
+ def platform_matches_target(platforms: list[str], target_platform: str | None) -> bool:
689
+ if target_platform is None:
690
+ return True
691
+ for platform in platforms:
692
+ normalized = platform.lower()
693
+ if target_platform == "ios" and "ios" in normalized:
694
+ return True
695
+ if target_platform == "macos" and "macos" in normalized:
696
+ return True
697
+ return False
698
+
699
+
700
+ def target_platform_note(entry: dict[str, Any], target_platform: str | None) -> str | None:
701
+ platforms = entry.get("toolkitPlatforms") or []
702
+ if not isinstance(platforms, list) or not platforms:
703
+ return None
704
+ platform_text = [str(platform) for platform in platforms]
705
+ if platform_matches_target(platform_text, target_platform):
706
+ return None
707
+ observed = ", ".join(platform_text)
708
+ return (
709
+ f"Only observed for {observed}; target platform is "
710
+ f"{target_platform_label(target_platform)}."
711
+ )
712
+
713
+
714
+ def has_toolkit_parameter_summary(entry: dict[str, Any]) -> bool:
715
+ summary = entry.get("toolkitParameterSummary")
716
+ if not isinstance(summary, dict):
717
+ return False
718
+ return "parameterCount" in summary or bool(summary.get("parameters"))
719
+
720
+
721
+ def parameter_metadata_note(
722
+ entry: dict[str, Any],
723
+ target_macos: int | None,
724
+ action_minimum_macos: int | None,
725
+ ) -> str | None:
726
+ if target_macos is None or target_macos >= PARAMETER_CATALOG_MIN_MACOS_MAJOR:
727
+ return None
728
+ if not has_toolkit_parameter_summary(entry):
729
+ return None
730
+ if action_minimum_macos is not None and action_minimum_macos > target_macos:
731
+ return None
732
+ return (
733
+ f"Parameter metadata is from OS {PARAMETER_CATALOG_MIN_MACOS_MAJOR} ToolKit; "
734
+ f"target macOS is {target_macos}."
735
+ )
736
+
737
+
738
+ def trigger_metadata_note(entry: dict[str, Any], target_macos: int | None) -> str | None:
739
+ if target_macos is None or target_macos >= TRIGGER_CATALOG_MIN_MACOS_MAJOR:
740
+ return None
741
+ if not isinstance(entry.get("toolkitTriggerSummary"), dict):
742
+ return None
743
+ return (
744
+ f"Trigger metadata is from OS {TRIGGER_CATALOG_MIN_MACOS_MAJOR} ToolKit; "
745
+ f"target macOS is {target_macos}."
746
+ )
747
+
748
+
749
+ def entry_min_macos(identifier: str, availability: dict[str, int | None]) -> int | None:
750
+ return availability.get(identifier)
751
+
752
+
753
+ def compact_entry(
754
+ identifier: str,
755
+ entry: dict[str, Any],
756
+ availability: dict[str, int | None],
757
+ target_macos: int | None,
758
+ target_platform: str | None,
759
+ ) -> dict[str, Any]:
760
+ minimum = entry_min_macos(identifier, availability)
761
+ return {
762
+ "identifier": identifier,
763
+ "status": entry.get("status"),
764
+ "minimumMacOSMajor": minimum,
765
+ "availabilityNote": target_note(minimum, target_macos),
766
+ "deprecationNote": deprecation_note(identifier),
767
+ "parameterMetadataMinimumMacOSMajor": (
768
+ PARAMETER_CATALOG_MIN_MACOS_MAJOR if has_toolkit_parameter_summary(entry) else None
769
+ ),
770
+ "parameterMetadataAvailabilityNote": parameter_metadata_note(
771
+ entry,
772
+ target_macos,
773
+ minimum,
774
+ ),
775
+ "triggerMetadataMinimumMacOSMajor": (
776
+ TRIGGER_CATALOG_MIN_MACOS_MAJOR
777
+ if isinstance(entry.get("toolkitTriggerSummary"), dict)
778
+ else None
779
+ ),
780
+ "triggerMetadataAvailabilityNote": trigger_metadata_note(entry, target_macos),
781
+ "platformAvailabilityNote": platform_availability_note(entry),
782
+ "targetPlatformAvailabilityNote": target_platform_note(entry, target_platform),
783
+ "name": entry.get("name"),
784
+ "pythonName": entry.get("pythonName"),
785
+ "toolRendererEmbedded": entry.get("toolRendererEmbedded"),
786
+ "toolRendererScriptingUtility": entry.get("toolRendererScriptingUtility"),
787
+ "summary": entry.get("summary"),
788
+ "parameters": entry.get("parameters") or [],
789
+ "toolkitPlatforms": entry.get("toolkitPlatforms") or [],
790
+ "toolkitToolType": entry.get("toolkitToolType"),
791
+ "toolkitParameterSummary": entry.get("toolkitParameterSummary") or {},
792
+ "toolkitTriggerSummary": entry.get("toolkitTriggerSummary") or {},
793
+ "workflowTriggerSample": entry.get("workflowTriggerSample") or {},
794
+ "sourceFunctions": entry.get("sourceFunctions") or {},
795
+ "sampleShortcuts": entry.get("sampleShortcuts") or [],
796
+ }
797
+
798
+
799
+ def print_markdown_entry(
800
+ identifier: str,
801
+ entry: dict[str, Any],
802
+ availability: dict[str, int | None],
803
+ target_macos: int | None,
804
+ target_platform: str | None,
805
+ ) -> None:
806
+ print(f"## {entry.get('name') or identifier}")
807
+ print()
808
+ print(f"- Identifier: `{identifier}`")
809
+ if entry.get("pythonName"):
810
+ print(f"- Apple Shortpy name: `{entry['pythonName']}`")
811
+ print(f"- Status: `{entry.get('status')}`")
812
+ minimum = entry_min_macos(identifier, availability)
813
+ if minimum is not None:
814
+ print(f"- Minimum target: macOS {minimum}+")
815
+ note = target_note(minimum, target_macos)
816
+ if note:
817
+ print(f"- Availability: {note}")
818
+ deprecated = deprecation_note(identifier)
819
+ if deprecated:
820
+ print(f"- Deprecation: {deprecated}")
821
+ parameter_note = parameter_metadata_note(entry, target_macos, minimum)
822
+ if parameter_note:
823
+ print(f"- Parameter metadata availability: {parameter_note}")
824
+ trigger_note = trigger_metadata_note(entry, target_macos)
825
+ if trigger_note:
826
+ print(f"- Trigger metadata availability: {trigger_note}")
827
+ platform_note = platform_availability_note(entry)
828
+ if platform_note:
829
+ print(f"- Platform availability: {platform_note}")
830
+ target_platform_message = target_platform_note(entry, target_platform)
831
+ if target_platform_message:
832
+ print(f"- Target platform availability: {target_platform_message}")
833
+ if entry.get("toolRendererEmbedded"):
834
+ flag = "ToolRenderer embedded"
835
+ if entry.get("toolRendererScriptingUtility"):
836
+ flag += ", scripting utility"
837
+ print(f"- Apple surface: {flag}")
838
+ if entry.get("summary"):
839
+ print(f"- Summary: {entry['summary']}")
840
+ samples = entry.get("sampleShortcuts") or []
841
+ if samples:
842
+ print(f"- Observed samples: {', '.join(samples)}")
843
+ parameters = entry.get("parameters") or []
844
+ if parameters:
845
+ print()
846
+ print("| WF key | Apple keyword | Required | Label |")
847
+ print("|--------|---------------|----------|-------|")
848
+ for parameter in parameters:
849
+ print(
850
+ "| `{}` | `{}` | {} | {} |".format(
851
+ parameter.get("wfKey") or "",
852
+ parameter.get("pythonKeyword") or "",
853
+ "yes" if parameter.get("required") else "no",
854
+ parameter.get("label") or "",
855
+ )
856
+ )
857
+ toolkit_summary = entry.get("toolkitParameterSummary") or {}
858
+ toolkit_parameters = toolkit_summary.get("parameters") if isinstance(toolkit_summary, dict) else None
859
+ if toolkit_parameters:
860
+ print()
861
+ print("| ToolKit key | Name | Type | Cases |")
862
+ print("|-------------|------|------|-------|")
863
+ for parameter in toolkit_parameters:
864
+ print(
865
+ "| `{}` | {} | `{}` | {} |".format(
866
+ parameter.get("key") or "",
867
+ markdown_cell(parameter.get("name") or ""),
868
+ "`, `".join(toolkit_parameter_type_names(parameter)),
869
+ toolkit_parameter_enum_case_preview(parameter),
870
+ )
871
+ )
872
+ trigger_summary = entry.get("toolkitTriggerSummary") or {}
873
+ trigger_parameters = trigger_summary.get("parameters") if isinstance(trigger_summary, dict) else None
874
+ if trigger_parameters:
875
+ print()
876
+ print("| Trigger key | Name | Type | Cases |")
877
+ print("|-------------|------|------|-------|")
878
+ for parameter in trigger_parameters:
879
+ print(
880
+ "| `{}` | {} | `{}` | {} |".format(
881
+ parameter.get("key") or "",
882
+ markdown_cell(parameter.get("name") or ""),
883
+ "`, `".join(toolkit_parameter_type_names(parameter)),
884
+ toolkit_parameter_enum_case_preview(parameter),
885
+ )
886
+ )
887
+ output_types = (
888
+ trigger_summary.get("outputTypeIdentifiers")
889
+ if isinstance(trigger_summary, dict)
890
+ else None
891
+ )
892
+ if output_types:
893
+ print()
894
+ print("- Trigger output types: " + ", ".join(f"`{item}`" for item in output_types))
895
+ workflow_trigger = entry.get("workflowTriggerSample") or {}
896
+ if isinstance(workflow_trigger, dict) and workflow_trigger:
897
+ print()
898
+ observed = workflow_trigger.get("observed")
899
+ if observed is True:
900
+ print("- Workflow trigger sample: observed exported `WFWorkflowTriggers` payload")
901
+ elif observed is False:
902
+ print("- Workflow trigger sample: no exported `WFWorkflowTriggers` sample yet")
903
+ template_status = workflow_trigger.get("templateStatus")
904
+ if template_status:
905
+ print(f"- Template status: `{template_status}`")
906
+ payload = workflow_trigger.get("workflowTrigger") or {}
907
+ if isinstance(payload, dict):
908
+ wf_identifier = payload.get("WFTriggerIdentifier")
909
+ if wf_identifier:
910
+ print(f"- `WFTriggerIdentifier`: `{wf_identifier}`")
911
+ serialized = payload.get("WFTriggerSerializedParameters")
912
+ if isinstance(serialized, dict):
913
+ keys = ", ".join(f"`{key}`" for key in sorted(serialized))
914
+ print(f"- Serialized parameter keys: {keys if keys else 'none'}")
915
+ notes = workflow_trigger.get("notes") or []
916
+ if notes:
917
+ print("- Sample notes: " + " ".join(str(note) for note in notes))
918
+
919
+
920
+ def markdown_cell(value: str) -> str:
921
+ return str(value).replace("|", "\\|").replace("\n", " ")
922
+
923
+
924
+ def toolkit_parameter_enum_case_preview(parameter: dict[str, Any], limit: int = 12) -> str:
925
+ enum_types = parameter.get("enumTypes")
926
+ if not isinstance(enum_types, list) or not enum_types:
927
+ return ""
928
+ rendered_types: list[str] = []
929
+ include_type_name = len(enum_types) > 1
930
+ for enum_type in enum_types:
931
+ if not isinstance(enum_type, dict):
932
+ continue
933
+ cases = enum_type.get("cases") or []
934
+ rendered_cases: list[str] = []
935
+ for case in cases[:limit]:
936
+ if not isinstance(case, dict):
937
+ continue
938
+ case_id = markdown_cell(case.get("id") or "")
939
+ title = markdown_cell(case.get("title") or "")
940
+ rendered_cases.append(
941
+ f"`{case_id}`" if not title or title == case_id else f"`{case_id}` ({title})"
942
+ )
943
+ if len(cases) > limit:
944
+ rendered_cases.append(f"... +{len(cases) - limit} more")
945
+ if not rendered_cases:
946
+ continue
947
+ rendered = ", ".join(rendered_cases)
948
+ if include_type_name:
949
+ type_name = markdown_cell(enum_type.get("typePythonName") or "")
950
+ rendered = f"`{type_name}`: {rendered}"
951
+ rendered_types.append(rendered)
952
+ return "<br>".join(rendered_types)
953
+
954
+
955
+ def main() -> int:
956
+ parser = argparse.ArgumentParser(description=__doc__)
957
+ group = parser.add_mutually_exclusive_group()
958
+ group.add_argument(
959
+ "--identifier",
960
+ help=(
961
+ "WF action/AppIntent identifier, automation trigger identifier, "
962
+ "or short WF action suffix, e.g. additemtolist"
963
+ ),
964
+ )
965
+ group.add_argument("--python-name", help="Apple Shortpy function name, e.g. com_apple_shortcuts_add_item_to_list")
966
+ group.add_argument("--query", help="Search names, summaries, keywords, parameters, and Python names")
967
+ parser.add_argument("--target-macos", default=None, help="Target macOS major version, auto, latest, or all")
968
+ parser.add_argument(
969
+ "--target-platform",
970
+ default=None,
971
+ help="Target platform: macos (default), ios/ipados, or all",
972
+ )
973
+ parser.add_argument("--limit", type=int, default=10, help="Maximum search/list results")
974
+ parser.add_argument("--list", action="store_true", help="List catalog entries")
975
+ parser.add_argument("--json", action="store_true", help="Print JSON")
976
+ args = parser.parse_args()
977
+
978
+ try:
979
+ target_macos = resolve_target_macos(args.target_macos)
980
+ target_platform = resolve_target_platform(args.target_platform)
981
+ except ValueError as exc:
982
+ print(str(exc))
983
+ return 2
984
+ catalog = load_catalog()
985
+ parameter_catalog = load_parameter_catalog()
986
+ enum_catalog = load_enum_catalog()
987
+ trigger_catalog = load_trigger_catalog()
988
+ workflow_trigger_catalog = load_workflow_trigger_catalog()
989
+ availability = load_identifier_min_macos()
990
+
991
+ results: list[tuple[str, dict[str, Any]]]
992
+ if args.identifier:
993
+ found = find_by_identifier(catalog, args.identifier)
994
+ if found:
995
+ found = (
996
+ found[0],
997
+ augment_with_parameter_summary(found[0], found[1], parameter_catalog),
998
+ )
999
+ else:
1000
+ found = find_parameter_by_identifier(parameter_catalog, args.identifier)
1001
+ if not found:
1002
+ found = find_trigger_by_identifier(trigger_catalog, args.identifier)
1003
+ results = [found] if found else []
1004
+ elif args.python_name:
1005
+ found = find_by_python_name(catalog, args.python_name)
1006
+ if found:
1007
+ found = (
1008
+ found[0],
1009
+ augment_with_parameter_summary(found[0], found[1], parameter_catalog),
1010
+ )
1011
+ else:
1012
+ found = find_parameter_by_python_name(parameter_catalog, args.python_name)
1013
+ if not found:
1014
+ found = find_trigger_by_python_name(trigger_catalog, args.python_name)
1015
+ results = [found] if found else []
1016
+ elif args.query:
1017
+ results = [
1018
+ (identifier, augment_with_parameter_summary(identifier, entry, parameter_catalog))
1019
+ for identifier, entry in search_entries(catalog, args.query, args.limit)
1020
+ ]
1021
+ seen = {identifier for identifier, _ in results}
1022
+ if len(results) < args.limit:
1023
+ for identifier, entry in search_parameter_entries(
1024
+ parameter_catalog,
1025
+ args.query,
1026
+ args.limit - len(results),
1027
+ ):
1028
+ if identifier not in seen:
1029
+ results.append((identifier, entry))
1030
+ seen.add(identifier)
1031
+ if len(results) < args.limit:
1032
+ for identifier, entry in search_trigger_entries(
1033
+ trigger_catalog,
1034
+ args.query,
1035
+ args.limit - len(results),
1036
+ ):
1037
+ if identifier not in seen:
1038
+ results.append((identifier, entry))
1039
+ seen.add(identifier)
1040
+ else:
1041
+ results = (
1042
+ [
1043
+ (identifier, augment_with_parameter_summary(identifier, entry, parameter_catalog))
1044
+ for identifier, entry in search_entries(catalog, "", args.limit)
1045
+ ]
1046
+ if args.list
1047
+ else []
1048
+ )
1049
+ results = [
1050
+ (identifier, filter_entry_for_target_platform(entry, target_platform))
1051
+ for identifier, entry in results
1052
+ ]
1053
+ results = [
1054
+ (identifier, augment_with_enum_cases(entry, enum_catalog))
1055
+ for identifier, entry in results
1056
+ ]
1057
+ results = [
1058
+ (
1059
+ identifier,
1060
+ augment_with_workflow_trigger_sample(
1061
+ identifier,
1062
+ entry,
1063
+ workflow_trigger_catalog,
1064
+ ),
1065
+ )
1066
+ for identifier, entry in results
1067
+ ]
1068
+
1069
+ if args.json:
1070
+ result_notes = [
1071
+ target_note(entry_min_macos(identifier, availability), target_macos)
1072
+ for identifier, _ in results
1073
+ ]
1074
+ unique_notes = sorted({note for note in result_notes if note})
1075
+ deprecation_notes = [
1076
+ deprecation_note(identifier)
1077
+ for identifier, _ in results
1078
+ ]
1079
+ unique_deprecation_notes = sorted({note for note in deprecation_notes if note})
1080
+ platform_notes = [
1081
+ platform_availability_note(entry)
1082
+ for _, entry in results
1083
+ ]
1084
+ unique_platform_notes = sorted({note for note in platform_notes if note})
1085
+ target_platform_notes = [
1086
+ target_platform_note(entry, target_platform)
1087
+ for _, entry in results
1088
+ ]
1089
+ unique_target_platform_notes = sorted({note for note in target_platform_notes if note})
1090
+ parameter_notes = [
1091
+ parameter_metadata_note(entry, target_macos, entry_min_macos(identifier, availability))
1092
+ for identifier, entry in results
1093
+ ]
1094
+ unique_parameter_notes = sorted({note for note in parameter_notes if note})
1095
+ trigger_notes = [
1096
+ trigger_metadata_note(entry, target_macos)
1097
+ for _, entry in results
1098
+ ]
1099
+ unique_trigger_notes = sorted({note for note in trigger_notes if note})
1100
+ payload = {
1101
+ "catalog": {
1102
+ "schemaVersion": catalog.get("schemaVersion"),
1103
+ "minimumMacOSMajor": catalog.get("minimumMacOSMajor"),
1104
+ "summary": catalog.get("summary"),
1105
+ "runtimePolicy": catalog.get("runtimePolicy"),
1106
+ },
1107
+ "enumCatalog": {
1108
+ "version": enum_catalog.get("version"),
1109
+ "enumTypeCount": enum_catalog.get("enumTypeCount"),
1110
+ "caseCount": enum_catalog.get("caseCount"),
1111
+ },
1112
+ "workflowTriggerCatalog": {
1113
+ "version": workflow_trigger_catalog.get("version"),
1114
+ "rootKey": workflow_trigger_catalog.get("rootKey"),
1115
+ "observedToolkitTriggerCount": workflow_trigger_catalog.get(
1116
+ "observedToolkitTriggerCount"
1117
+ ),
1118
+ "unobservedToolkitTriggerCount": workflow_trigger_catalog.get(
1119
+ "unobservedToolkitTriggerCount"
1120
+ ),
1121
+ "minimumMacOSMajor": WORKFLOW_TRIGGER_CATALOG_MIN_MACOS_MAJOR,
1122
+ },
1123
+ "targetMacOSMajor": target_macos,
1124
+ "targetPlatform": target_platform_label(target_platform),
1125
+ "availabilityNote": unique_notes[0] if len(unique_notes) == 1 else None,
1126
+ "deprecationNote": (
1127
+ unique_deprecation_notes[0]
1128
+ if len(unique_deprecation_notes) == 1
1129
+ else None
1130
+ ),
1131
+ "parameterMetadataAvailabilityNote": (
1132
+ unique_parameter_notes[0] if len(unique_parameter_notes) == 1 else None
1133
+ ),
1134
+ "triggerMetadataAvailabilityNote": (
1135
+ unique_trigger_notes[0] if len(unique_trigger_notes) == 1 else None
1136
+ ),
1137
+ "platformAvailabilityNote": (
1138
+ unique_platform_notes[0] if len(unique_platform_notes) == 1 else None
1139
+ ),
1140
+ "targetPlatformAvailabilityNote": (
1141
+ unique_target_platform_notes[0]
1142
+ if len(unique_target_platform_notes) == 1
1143
+ else None
1144
+ ),
1145
+ "results": [
1146
+ compact_entry(identifier, entry, availability, target_macos, target_platform)
1147
+ for identifier, entry in results
1148
+ ],
1149
+ }
1150
+ print(json.dumps(payload, indent=2, sort_keys=True))
1151
+ return 0 if results else 1
1152
+
1153
+ if not results:
1154
+ print("No Apple-derived grounding entry found.")
1155
+ return 1
1156
+ for index, (identifier, entry) in enumerate(results):
1157
+ if index:
1158
+ print()
1159
+ print_markdown_entry(identifier, entry, availability, target_macos, target_platform)
1160
+ return 0
1161
+
1162
+
1163
+ if __name__ == "__main__":
1164
+ raise SystemExit(main())