opencode-skills-collection 4.1.18 → 4.1.20

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 (79) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +20 -1
  2. package/bundled-skills/ai-headshot-studio/SKILL.md +107 -99
  3. package/bundled-skills/ai-image-generation-studio/SKILL.md +108 -93
  4. package/bundled-skills/ai-logo-maker/SKILL.md +108 -93
  5. package/bundled-skills/ai-multilingual-dubbing/SKILL.md +108 -93
  6. package/bundled-skills/ai-photo-restyler/SKILL.md +108 -93
  7. package/bundled-skills/ai-podcast-voiceover/SKILL.md +108 -93
  8. package/bundled-skills/aident-loadout/SKILL.md +88 -0
  9. package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +27 -254
  10. package/bundled-skills/antigravity-maintainer-batch-release/agents/openai.yaml +1 -1
  11. package/bundled-skills/antigravity-maintainer-batch-release/references/component-validation.md +18 -0
  12. package/bundled-skills/antigravity-maintainer-batch-release/references/core-preview.md +20 -0
  13. package/bundled-skills/antigravity-maintainer-batch-release/references/maintenance-contract.md +83 -0
  14. package/bundled-skills/antigravity-maintainer-batch-release/references/maintenance-cycle.md +149 -0
  15. package/bundled-skills/antigravity-maintainer-batch-release/references/public-surfaces.md +22 -0
  16. package/bundled-skills/antigravity-maintainer-batch-release/references/skillspector.md +40 -0
  17. package/bundled-skills/apify-facebook-ads-library-scraper/SKILL.md +197 -0
  18. package/bundled-skills/apify-facebook-ads-library-scraper/references/gotchas.md +45 -0
  19. package/bundled-skills/apify-linkedin-jobs-scraper/SKILL.md +181 -0
  20. package/bundled-skills/apify-linkedin-jobs-scraper/references/gotchas.md +57 -0
  21. package/bundled-skills/apify-tiktok-scraper/SKILL.md +199 -0
  22. package/bundled-skills/apify-tiktok-scraper/references/gotchas.md +45 -0
  23. package/bundled-skills/arcmira-find-quotes/SKILL.md +5 -0
  24. package/bundled-skills/beatra/SKILL.md +109 -90
  25. package/bundled-skills/beatra-ai-video-studio/SKILL.md +110 -90
  26. package/bundled-skills/curlshot/SKILL.md +139 -0
  27. package/bundled-skills/datacircle/SKILL.md +159 -98
  28. package/bundled-skills/deed-page/SKILL.md +161 -0
  29. package/bundled-skills/design-token-contrast/SKILL.md +128 -0
  30. package/bundled-skills/design-token-contrast/references/contraste-checker.md +330 -0
  31. package/bundled-skills/ecommerce-listing-image-set/SKILL.md +109 -90
  32. package/bundled-skills/goal-instruction-writer/LICENSE +21 -0
  33. package/bundled-skills/goal-instruction-writer/SKILL.md +275 -0
  34. package/bundled-skills/goal-instruction-writer/references/contribution-walkthrough.md +9 -0
  35. package/bundled-skills/goal-instruction-writer/references/drift-control.md +160 -0
  36. package/bundled-skills/goal-instruction-writer/references/examples.md +220 -0
  37. package/bundled-skills/goal-instruction-writer/references/goal-contract.md +169 -0
  38. package/bundled-skills/goal-instruction-writer/references/loop-preview.md +113 -0
  39. package/bundled-skills/keenable-search-mcp/SKILL.md +176 -0
  40. package/bundled-skills/krexa/SKILL.md +221 -0
  41. package/bundled-skills/krexa/references/limits.md +42 -0
  42. package/bundled-skills/krexa/references/setup.md +60 -0
  43. package/bundled-skills/krexa/references/tools.md +59 -0
  44. package/bundled-skills/llm-cost-optimization/SKILL.md +1 -1
  45. package/bundled-skills/llmops-platform-engineering/SKILL.md +1 -1
  46. package/bundled-skills/multi-tenant-llm-hosting/SKILL.md +1 -1
  47. package/bundled-skills/music-generation-studio/SKILL.md +109 -90
  48. package/bundled-skills/outreachgenie-list-audit/LICENSE +21 -0
  49. package/bundled-skills/outreachgenie-list-audit/SKILL.md +60 -0
  50. package/bundled-skills/outreachgenie-list-audit/assets/fictional-snapshot.json +256 -0
  51. package/bundled-skills/outreachgenie-list-audit/references/capture.md +53 -0
  52. package/bundled-skills/outreachgenie-list-audit/references/checks.md +35 -0
  53. package/bundled-skills/outreachgenie-list-audit/references/setup.md +25 -0
  54. package/bundled-skills/outreachgenie-list-audit/scripts/audit.py +766 -0
  55. package/bundled-skills/outreachgenie-list-audit/scripts/test_audit.py +360 -0
  56. package/bundled-skills/pangolinfo-amazon-data-research/SKILL.md +170 -0
  57. package/bundled-skills/poster-design-studio/SKILL.md +109 -90
  58. package/bundled-skills/ppl-personal-crm/SKILL.md +118 -0
  59. package/bundled-skills/product-photo-studio/SKILL.md +109 -90
  60. package/bundled-skills/project-memory-log/LICENSE +21 -0
  61. package/bundled-skills/project-memory-log/SKILL.md +110 -0
  62. package/bundled-skills/project-memory-log/references/example-document-catalog.md +22 -0
  63. package/bundled-skills/project-memory-log/references/example-notes.md +16 -0
  64. package/bundled-skills/project-memory-log/references/example-operation-log.md +46 -0
  65. package/bundled-skills/project-memory-log/references/record-formats.md +77 -0
  66. package/bundled-skills/solidping/SKILL.md +87 -0
  67. package/bundled-skills/story-bible-continuity/SKILL.md +149 -0
  68. package/bundled-skills/story-bible-continuity/references/sbc-checker.md +367 -0
  69. package/bundled-skills/story-bible-continuity/references/story-bible-schema.md +91 -0
  70. package/bundled-skills/suno-lyrics-to-song/SKILL.md +109 -90
  71. package/bundled-skills/taasky/SKILL.md +376 -0
  72. package/bundled-skills/talking-avatar-video/SKILL.md +109 -90
  73. package/bundled-skills/uiverify-triage-visual-changes/SKILL.md +304 -0
  74. package/bundled-skills/unpaid-intern/SKILL.md +159 -0
  75. package/bundled-skills/viral-video-teardown-remake/SKILL.md +109 -90
  76. package/bundled-skills/voice-cloning-studio/SKILL.md +109 -90
  77. package/bundled-skills/voiceover-narration-studio/SKILL.md +109 -90
  78. package/package.json +1 -1
  79. package/skills_index.json +654 -24
@@ -0,0 +1,766 @@
1
+ """Audit an explicitly captured OutreachGenie snapshot. No network/API calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import html
7
+ import json
8
+ import math
9
+ import os
10
+ import re
11
+ from collections import Counter
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from uuid import UUID
15
+
16
+ VERSION = "0.1.0"
17
+ BUILTINS = {
18
+ "first_name",
19
+ "last_name",
20
+ "company",
21
+ "title",
22
+ "email",
23
+ "phone",
24
+ "linkedin_url",
25
+ }
26
+ PRIVATE_FIELDS = {"email", "phone"}
27
+ TOKEN = re.compile(r"{{\s*([^{}]+?)\s*}}")
28
+ ALLOWED_TOOLS = {
29
+ "get_workspace_context",
30
+ "get_flow",
31
+ "list_flow_template_fields",
32
+ "get_prospect_list",
33
+ "list_prospects_in_list",
34
+ }
35
+ LIMITS = {
36
+ "send_connection_request": ("invite_note", 200, False, "utf16"),
37
+ "send_message": ("message", 1000, True, "code_points"),
38
+ "comment_post": ("comment_text", 1000, True, "code_points"),
39
+ }
40
+ MAX_BYTES = 5 * 1024 * 1024
41
+ MAX_RECORDS = 1000
42
+ MAX_FINDINGS = 10000
43
+
44
+
45
+ class InputError(ValueError):
46
+ pass
47
+
48
+
49
+ def require(condition, message):
50
+ if not condition:
51
+ raise InputError(message)
52
+
53
+
54
+ def load_snapshot(path):
55
+ """Read bounded, unambiguous UTF-8 JSON without printing captured values."""
56
+
57
+ def object_from_pairs(pairs):
58
+ result = {}
59
+ for key, value in pairs:
60
+ require(key not in result, "Snapshot contains duplicate JSON object keys.")
61
+ result[key] = value
62
+ return result
63
+
64
+ def finite_float(value):
65
+ parsed = float(value)
66
+ require(math.isfinite(parsed), "Snapshot contains a non-finite number.")
67
+ return parsed
68
+
69
+ def reject_constant(_value):
70
+ raise InputError("Snapshot contains a non-finite number.")
71
+
72
+ with path.open("rb") as handle:
73
+ raw = handle.read(MAX_BYTES + 1)
74
+ require(len(raw) <= MAX_BYTES, "Snapshot exceeds 5 MiB.")
75
+ try:
76
+ snapshot = json.loads(
77
+ raw.decode("utf-8"),
78
+ object_pairs_hook=object_from_pairs,
79
+ parse_float=finite_float,
80
+ parse_constant=reject_constant,
81
+ )
82
+ pending = [snapshot]
83
+ while pending:
84
+ value = pending.pop()
85
+ if isinstance(value, str):
86
+ value.encode("utf-8")
87
+ elif isinstance(value, dict):
88
+ pending.extend(value.keys())
89
+ pending.extend(value.values())
90
+ elif isinstance(value, list):
91
+ pending.extend(value)
92
+ return snapshot
93
+ except InputError:
94
+ raise
95
+ except (UnicodeError, ValueError, RecursionError) as error:
96
+ raise InputError(
97
+ "Snapshot must be valid UTF-8 JSON with supported nesting and numeric values."
98
+ ) from error
99
+
100
+
101
+ def uuid(value):
102
+ require(isinstance(value, str), "Expected a UUID string.")
103
+ try:
104
+ return str(UUID(value))
105
+ except ValueError as error:
106
+ raise InputError("Invalid UUID in snapshot.") from error
107
+
108
+
109
+ def normalize_key(raw):
110
+ key = raw.strip()
111
+ if not key or any(c.isspace() for c in key):
112
+ raise ValueError("empty_or_spaced_token")
113
+ if key.startswith("custom_fields."):
114
+ custom = key[len("custom_fields.") :].strip().lower()
115
+ if not re.fullmatch(r"[a-z0-9_]{1,64}", custom):
116
+ raise ValueError("invalid_custom_token")
117
+ return "custom", custom
118
+ if not re.fullmatch(r"[a-z][a-z0-9_]*", key):
119
+ raise ValueError("invalid_token")
120
+ if key in BUILTINS:
121
+ return "builtin", key
122
+ if key == "note":
123
+ raise ValueError("note_is_not_a_template_field")
124
+ if not re.fullmatch(r"[a-z0-9_]{1,64}", key):
125
+ raise ValueError("invalid_custom_token")
126
+ return "custom", key
127
+
128
+
129
+ def parse_template(text):
130
+ refs, errors, spans = [], [], []
131
+ for match in TOKEN.finditer(text):
132
+ spans.append(match.span())
133
+ try:
134
+ ref = normalize_key(match.group(1))
135
+ if ref not in refs:
136
+ refs.append(ref)
137
+ except ValueError as error:
138
+ errors.append(str(error))
139
+ remaining = text
140
+ for start, end in reversed(spans):
141
+ remaining = remaining[:start] + remaining[end:]
142
+ if "{{" in remaining or "}}" in remaining:
143
+ errors.append("unbalanced_template_braces")
144
+ return refs, sorted(set(errors))
145
+
146
+
147
+ def saved_value(record, ref):
148
+ kind, key = ref
149
+ if kind == "builtin":
150
+ if key not in record:
151
+ return None
152
+ value = record[key]
153
+ else:
154
+ if "custom_fields" not in record or not isinstance(
155
+ record["custom_fields"], dict
156
+ ):
157
+ return None
158
+ value = record["custom_fields"].get(key, "")
159
+ if value is not None and not isinstance(value, (str, bool, int, float)):
160
+ return None
161
+ return str(value or "")
162
+
163
+
164
+ def render(text, record):
165
+ """Runtime-compatible single substitution pass; caller checks syntax/coverage."""
166
+
167
+ def replacement(match):
168
+ try:
169
+ return saved_value(record, normalize_key(match.group(1))) or ""
170
+ except ValueError:
171
+ return ""
172
+
173
+ return TOKEN.sub(replacement, text).strip()
174
+
175
+
176
+ def length(text, unit):
177
+ return (
178
+ sum(2 if ord(c) > 0xFFFF else 1 for c in text) if unit == "utf16" else len(text)
179
+ )
180
+
181
+
182
+ def rendered_length(text, record, unit):
183
+ """Measure stripped single-pass output without allocating expanded messages."""
184
+ cached = {}
185
+
186
+ def stats(value):
187
+ size = length(value, unit)
188
+ leading = length(value[: len(value) - len(value.lstrip())], unit)
189
+ trailing = length(value[len(value.rstrip()) :], unit)
190
+ return size, leading, trailing
191
+
192
+ def chunks():
193
+ end = 0
194
+ for match in TOKEN.finditer(text):
195
+ yield stats(text[end : match.start()])
196
+ ref = normalize_key(match.group(1))
197
+ if ref not in cached:
198
+ cached[ref] = stats(saved_value(record, ref) or "")
199
+ yield cached[ref]
200
+ end = match.end()
201
+ yield stats(text[end:])
202
+
203
+ started, total, trailing_total = False, 0, 0
204
+ for size, leading, trailing in chunks():
205
+ if not started:
206
+ if leading == size:
207
+ continue
208
+ started = True
209
+ total += size - leading
210
+ trailing_total = trailing
211
+ else:
212
+ total += size
213
+ trailing_total = trailing_total + size if leading == size else trailing
214
+ return total - trailing_total
215
+
216
+
217
+ def text_fields(nodes):
218
+ require(
219
+ isinstance(nodes, list) and len(nodes) <= 200,
220
+ "Flow nodes must be a list of at most 200 nodes.",
221
+ )
222
+ seen = set()
223
+ fields, issues, unchecked = [], [], []
224
+ for node in nodes:
225
+ require(isinstance(node, dict), "Flow node must be an object.")
226
+ nid = node.get("id")
227
+ require(
228
+ isinstance(nid, str) and 0 < len(nid) <= 200 and nid not in seen,
229
+ "Missing or duplicate flow node ID.",
230
+ )
231
+ seen.add(nid)
232
+ if node.get("type") != "action":
233
+ continue
234
+ data = node.get("data")
235
+ require(isinstance(data, dict), "Action node data must be an object.")
236
+ action, config = data.get("actionKey"), data.get("config")
237
+ require(
238
+ isinstance(action, str) and isinstance(config, dict),
239
+ "Action key/config is missing.",
240
+ )
241
+ if action == "message_sequence":
242
+ messages = config.get("messages")
243
+ if not isinstance(messages, list) or not messages:
244
+ issues.append((nid, "unconfigured_message_sequence"))
245
+ continue
246
+ if not 2 <= len(messages) <= 10:
247
+ issues.append((nid, "message_sequence_count_outside_product_limit"))
248
+ sequence_ids = [
249
+ entry.get("id") if isinstance(entry, dict) else None
250
+ for entry in messages
251
+ ]
252
+ if any(
253
+ not isinstance(value, str) or not value.strip()
254
+ for value in sequence_ids
255
+ ) or len({str(value) for value in sequence_ids}) != len(sequence_ids):
256
+ issues.append((nid, "message_sequence_ids_invalid"))
257
+ for index, message in enumerate(messages):
258
+ if not isinstance(message, dict) or not isinstance(
259
+ message.get("message"), str
260
+ ):
261
+ issues.append((nid, "message_text_unavailable"))
262
+ continue
263
+ fields.append(
264
+ (
265
+ nid,
266
+ action,
267
+ f"messages[{index}].message",
268
+ message["message"],
269
+ 1000,
270
+ True,
271
+ "code_points",
272
+ )
273
+ )
274
+ elif action in LIMITS:
275
+ name, limit, required, unit = LIMITS[action]
276
+ camel = name.split("_")[0] + "".join(x.title() for x in name.split("_")[1:])
277
+ if camel != name and name in config and camel in config:
278
+ issues.append((nid, "ambiguous_text_field_aliases"))
279
+ continue
280
+ value = config.get(name)
281
+ if value is None:
282
+ value = config.get(camel)
283
+ if value is None and not required:
284
+ value = ""
285
+ if not isinstance(value, str):
286
+ issues.append((nid, "message_text_unavailable"))
287
+ continue
288
+ fields.append((nid, action, name, value, limit, required, unit))
289
+ else:
290
+ unchecked.append({"node_id": nid, "action_key": action})
291
+ require(len(fields) <= 500, "Snapshot exceeds 500 outbound text fields.")
292
+ return fields, issues, unchecked
293
+
294
+
295
+ def audit(snapshot):
296
+ require(
297
+ isinstance(snapshot, dict)
298
+ and type(snapshot.get("schema_version")) is int
299
+ and snapshot.get("schema_version") == 1,
300
+ "Expected snapshot schema_version 1.",
301
+ )
302
+ require(
303
+ isinstance(snapshot.get("source_kind"), str)
304
+ and snapshot.get("source_kind") in {"fictional_fixture", "mcp_snapshot"},
305
+ "Declare fictional_fixture or mcp_snapshot.",
306
+ )
307
+ captured = snapshot.get("captured_at")
308
+ require(isinstance(captured, str), "captured_at is required.")
309
+ try:
310
+ parsed_date = datetime.fromisoformat(captured.replace("Z", "+00:00"))
311
+ require(parsed_date.tzinfo is not None, "captured_at requires a timezone.")
312
+ except ValueError as error:
313
+ raise InputError("captured_at must be an ISO timestamp.") from error
314
+ workspace_id, flow_id = (
315
+ uuid(snapshot.get("workspace_id")),
316
+ uuid(snapshot.get("flow_id")),
317
+ )
318
+ ids = snapshot.get("selected_list_ids")
319
+ require(
320
+ isinstance(ids, list) and 1 <= len(ids) <= 20, "Select one to twenty list IDs."
321
+ )
322
+ ids = [uuid(x) for x in ids]
323
+ require(len(set(ids)) == len(ids), "Selected list IDs must be unique.")
324
+ calls = snapshot.get("calls")
325
+ require(
326
+ isinstance(calls, list) and len(calls) <= 1000,
327
+ "Expected at most 1000 captured tool calls.",
328
+ )
329
+ grouped = {name: [] for name in ALLOWED_TOOLS}
330
+ for call in calls:
331
+ require(
332
+ isinstance(call, dict)
333
+ and isinstance(call.get("tool"), str)
334
+ and call.get("tool") in ALLOWED_TOOLS,
335
+ "Only the five documented read tools belong in this snapshot.",
336
+ )
337
+ require(
338
+ isinstance(call.get("arguments"), dict)
339
+ and isinstance(call.get("result"), dict),
340
+ "Call arguments and unwrapped result must be objects.",
341
+ )
342
+ grouped[call["tool"]].append(call)
343
+ for singleton in ["get_workspace_context", "get_flow", "list_flow_template_fields"]:
344
+ require(
345
+ len(grouped[singleton]) == 1, f"Exactly one {singleton} result is required."
346
+ )
347
+ context = grouped["get_workspace_context"][0]
348
+ require(
349
+ isinstance(context["result"].get("workspace"), dict),
350
+ "Workspace context was not captured.",
351
+ )
352
+ require(
353
+ uuid(context["result"].get("workspace", {}).get("id")) == workspace_id,
354
+ "Connected workspace does not match requested workspace.",
355
+ )
356
+ flow_call = grouped["get_flow"][0]
357
+ require(
358
+ uuid(flow_call["arguments"].get("flow_id")) == flow_id
359
+ and uuid(flow_call["result"].get("id")) == flow_id,
360
+ "Flow ID mismatch.",
361
+ )
362
+ schema_fields = grouped["list_flow_template_fields"][0]["result"].get(
363
+ "built_in_fields"
364
+ )
365
+ require(
366
+ isinstance(schema_fields, list)
367
+ and all(
368
+ isinstance(x, dict) and isinstance(x.get("key"), str) for x in schema_fields
369
+ ),
370
+ "Template-field catalog is missing.",
371
+ )
372
+ require(
373
+ {x.get("key") for x in schema_fields} == BUILTINS,
374
+ "Built-in template catalog changed; review the helper contract before continuing.",
375
+ )
376
+ fields, config_issues, unchecked = text_fields(flow_call["result"].get("nodes"))
377
+ findings = []
378
+
379
+ def add(code, level="review", **evidence):
380
+ require(
381
+ len(findings) < MAX_FINDINGS,
382
+ "Snapshot produces over 10000 findings; reduce the selected-list scope and rerun.",
383
+ )
384
+ findings.append({"code": code, "level": level, **evidence})
385
+
386
+ for node_id, code in config_issues:
387
+ add(code, "unsupported", node_id=node_id)
388
+ refs_by_field, custom_keys = {}, set()
389
+ for node_id, action, field, text, limit, required, unit in fields:
390
+ require(
391
+ len(text) <= 100000, "Individual template exceeds the audit input limit."
392
+ )
393
+ refs, errors = parse_template(text)
394
+ refs_by_field[(node_id, field)] = (refs, errors)
395
+ custom_keys.update(key for kind, key in refs if kind == "custom")
396
+ for error in errors:
397
+ add(error, "template_issue", node_id=node_id, field=field)
398
+ if action == "send_connection_request" and length(text.strip(), "utf16") > 200:
399
+ add(
400
+ "raw_invitation_note_exceeds_product_limit",
401
+ "template_issue",
402
+ node_id=node_id,
403
+ field=field,
404
+ length=length(text.strip(), "utf16"),
405
+ limit=200,
406
+ unit="utf16",
407
+ )
408
+ if action in {"send_message", "message_sequence"} and len(text.strip()) > 1000:
409
+ add(
410
+ "raw_message_exceeds_product_limit",
411
+ "template_issue",
412
+ node_id=node_id,
413
+ field=field,
414
+ length=len(text.strip()),
415
+ limit=1000,
416
+ unit="code_points",
417
+ )
418
+ if required and not text.strip():
419
+ add(
420
+ "required_raw_text_empty",
421
+ "template_issue",
422
+ node_id=node_id,
423
+ field=field,
424
+ )
425
+ for kind, key in refs:
426
+ if kind == "builtin" and key in PRIVATE_FIELDS:
427
+ add(
428
+ "privacy_sensitive_template_field",
429
+ "context",
430
+ node_id=node_id,
431
+ field=field,
432
+ template_field=key,
433
+ )
434
+ details, pages = {}, {lid: [] for lid in ids}
435
+ for call in grouped["get_prospect_list"]:
436
+ lid = uuid(call["arguments"].get("list_id"))
437
+ require(
438
+ lid in ids and lid not in details and uuid(call["result"].get("id")) == lid,
439
+ "Unexpected, duplicate or mismatched list detail.",
440
+ )
441
+ details[lid] = call["result"]
442
+ require(set(details) == set(ids), "Each selected list requires one detail result.")
443
+ for call in grouped["list_prospects_in_list"]:
444
+ lid = uuid(call["arguments"].get("list_id"))
445
+ require(lid in ids, "Prospect page belongs to an unselected list.")
446
+ pages[lid].append(call)
447
+ coverage, records_by_list, total_records = [], {}, 0
448
+ for lid in ids:
449
+ detail = details[lid]
450
+ metadata = detail.get("custom_field_metadata")
451
+ require(
452
+ isinstance(metadata, list) and all(isinstance(x, dict) for x in metadata),
453
+ "List field metadata was not captured.",
454
+ )
455
+ available = {x.get("key") for x in metadata if isinstance(x.get("key"), str)}
456
+ for key in sorted(custom_keys - available):
457
+ add(
458
+ "custom_field_missing_from_list_metadata",
459
+ "template_issue",
460
+ list_id=lid,
461
+ template_field=key,
462
+ )
463
+ active_count = detail.get("active_campaign_count")
464
+ if type(active_count) is int and active_count > 0:
465
+ add(
466
+ "list_has_active_campaigns",
467
+ "context",
468
+ list_id=lid,
469
+ active_campaign_count=active_count,
470
+ )
471
+ elif type(active_count) is not int or active_count < 0:
472
+ add("active_campaign_context_unavailable", "unknown", list_id=lid)
473
+ expected = detail.get("prospect_count")
474
+ require(
475
+ type(expected) is int and expected >= 0,
476
+ "List prospect_count must be a nonnegative integer.",
477
+ )
478
+ collected, issues, seen_ids = [], [], set()
479
+ ordered = pages[lid]
480
+ next_expected, final_seen, first_size = 1, False, None
481
+ for call in ordered:
482
+ args, result = call["arguments"], call["result"]
483
+ query = args.get("query")
484
+ require(
485
+ isinstance(query, dict) and set(query) <= {"page", "page_size"},
486
+ "Capture an unfiltered page with explicit page/page_size.",
487
+ )
488
+ page, size = query.get("page"), query.get("page_size")
489
+ require(
490
+ type(page) is int
491
+ and page >= 1
492
+ and type(size) is int
493
+ and 1 <= size <= 100,
494
+ "Invalid page/page_size arguments.",
495
+ )
496
+ if first_size is None:
497
+ first_size = size
498
+ elif size != first_size:
499
+ issues.append("page_size_changed")
500
+ items, info = result.get("items"), result.get("pagination")
501
+ require(
502
+ isinstance(items, list) and isinstance(info, dict),
503
+ "Items and normalized pagination are required.",
504
+ )
505
+ require(len(items) <= size, "Page contains more records than requested.")
506
+ if page != next_expected or final_seen:
507
+ issues.append("noncontiguous_or_extra_pages")
508
+ if (
509
+ any(
510
+ type(info.get(key)) is not int
511
+ for key in ["page", "page_size", "total"]
512
+ )
513
+ or info.get("page") != page
514
+ or info.get("page_size") != size
515
+ or info.get("total") != expected
516
+ ):
517
+ issues.append("pagination_or_total_mismatch")
518
+ has_more = info.get("has_more")
519
+ if type(has_more) is not bool:
520
+ issues.append("missing_has_more")
521
+ elif (has_more and type(info.get("next_page")) is not int) or info.get(
522
+ "next_page"
523
+ ) != (page + 1 if has_more else None):
524
+ issues.append("next_page_mismatch")
525
+ previous = info.get("previous_page")
526
+ if (page > 1 and type(previous) is not int) or previous != (
527
+ page - 1 if page > 1 else None
528
+ ):
529
+ issues.append("previous_page_mismatch")
530
+ if type(has_more) is bool and has_more != (page * size < expected):
531
+ issues.append("has_more_disagrees_with_total")
532
+ if len(items) != max(0, min(size, expected - (page - 1) * size)):
533
+ issues.append("unexpected_page_length")
534
+ if has_more is False:
535
+ final_seen = True
536
+ next_expected = page + 1
537
+ for item in items:
538
+ require(isinstance(item, dict), "Prospect item must be an object.")
539
+ pid = uuid(item.get("id"))
540
+ if pid in seen_ids:
541
+ issues.append("duplicate_record_across_pages")
542
+ continue
543
+ seen_ids.add(pid)
544
+ collected.append({**item, "id": pid})
545
+ total_records += len(collected)
546
+ require(
547
+ total_records <= MAX_RECORDS,
548
+ f"Snapshot exceeds {MAX_RECORDS} selected-list memberships; capture a bounded scope and report partial coverage.",
549
+ )
550
+ if not ordered or not final_seen or len(collected) != expected:
551
+ issues.append("incomplete_collection")
552
+ for issue in sorted(set(issues)):
553
+ add(issue, "coverage_issue", list_id=lid)
554
+ coverage.append(
555
+ {
556
+ "list_id": lid,
557
+ "reported_memberships": expected,
558
+ "processed_memberships": len(collected),
559
+ "pages": len(ordered),
560
+ "complete": not issues,
561
+ }
562
+ )
563
+ records_by_list[lid] = collected
564
+ if expected == 0:
565
+ add("empty_selected_list", "context", list_id=lid)
566
+ for lid, records in records_by_list.items():
567
+ for record in records:
568
+ pid = uuid(record["id"])
569
+ for node_id, action, field, text, limit, required, unit in fields:
570
+ refs, errors = refs_by_field[(node_id, field)]
571
+ if errors:
572
+ continue
573
+ unknown = False
574
+ for ref in refs:
575
+ value = saved_value(record, ref)
576
+ key = f"custom_fields.{ref[1]}" if ref[0] == "custom" else ref[1]
577
+ evidence = {
578
+ "list_id": lid,
579
+ "prospect_id": pid,
580
+ "node_id": node_id,
581
+ "field": field,
582
+ "template_field": key,
583
+ }
584
+ if value is None:
585
+ unknown = True
586
+ add(
587
+ "saved_value_not_captured_or_unsupported",
588
+ "unknown",
589
+ **evidence,
590
+ )
591
+ elif value == "":
592
+ add("saved_template_value_empty", "review", **evidence)
593
+ elif "{{" in value or "}}" in value:
594
+ add(
595
+ "saved_value_contains_literal_braces", "context", **evidence
596
+ )
597
+ if unknown:
598
+ continue
599
+ measured_length = rendered_length(text, record, unit)
600
+ evidence = {
601
+ "list_id": lid,
602
+ "prospect_id": pid,
603
+ "node_id": node_id,
604
+ "field": field,
605
+ }
606
+ if measured_length == 0:
607
+ add(
608
+ "required_text_renders_empty"
609
+ if required
610
+ else "optional_note_renders_empty",
611
+ "template_issue" if required else "context",
612
+ **evidence,
613
+ )
614
+ elif measured_length > limit:
615
+ add(
616
+ "rendered_text_exceeds_product_limit",
617
+ "template_issue",
618
+ length=measured_length,
619
+ limit=limit,
620
+ unit=unit,
621
+ **evidence,
622
+ )
623
+ if not fields:
624
+ add("no_supported_outbound_text_fields", "unsupported")
625
+ counts = dict(sorted(Counter(f["code"] for f in findings).items()))
626
+ return {
627
+ "audit_version": VERSION,
628
+ "source_kind": snapshot["source_kind"],
629
+ "captured_at": captured,
630
+ "workspace_id": workspace_id,
631
+ "flow_id": flow_id,
632
+ "coverage": coverage,
633
+ "all_selected_memberships_captured": all(x["complete"] for x in coverage),
634
+ "unique_prospects_processed": len(
635
+ {x["id"] for records in records_by_list.values() for x in records}
636
+ ),
637
+ "outbound_text_fields_checked": len(fields),
638
+ "unchecked_actions": unchecked,
639
+ "counts": counts,
640
+ "findings": findings,
641
+ "limits": [
642
+ "Supplied snapshot only; authentication and live workspace state are not independently verified.",
643
+ "No graph reachability, schedule, reply-state, contact permission, profile validity or delivery audit.",
644
+ "Findings omit raw saved values and rendered message text; record IDs and field names may still be sensitive.",
645
+ "A complete snapshot or zero findings is not approval to activate or send.",
646
+ "Field truth, relevance and whether a message is appropriate require human judgement.",
647
+ ],
648
+ }
649
+
650
+
651
+ def markdown(report):
652
+ def cell(value):
653
+ return (
654
+ html.escape(str(value))
655
+ .replace("\\", "\\\\")
656
+ .replace("|", "\\|")
657
+ .replace("\n", " ")
658
+ .replace("\r", " ")
659
+ .replace("`", "\\`")
660
+ .replace("[", "\\[")
661
+ .replace("]", "\\]")
662
+ )
663
+
664
+ lines = [
665
+ "# OutreachGenie saved-list/flow audit",
666
+ "",
667
+ f"Source: {report['source_kind']}; captured: {report['captured_at']}",
668
+ "",
669
+ "This report checks a supplied snapshot. It does not approve sending.",
670
+ "",
671
+ "## Coverage",
672
+ "",
673
+ "| List ID | Reported memberships | Processed | Complete |",
674
+ "| --- | ---: | ---: | --- |",
675
+ ]
676
+ for item in report["coverage"]:
677
+ lines.append(
678
+ f"| {item['list_id']} | {item['reported_memberships']} | {item['processed_memberships']} | {item['complete']} |"
679
+ )
680
+ lines += [
681
+ "",
682
+ "## Findings",
683
+ "",
684
+ "| Finding | Level | List ID | Prospect ID | Node | Text field | Template field | Detail |",
685
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
686
+ ]
687
+ for item in report["findings"]:
688
+ detail = ""
689
+ if "length" in item:
690
+ detail = f"{item['length']} / {item['limit']} {item['unit']}"
691
+ elif "active_campaign_count" in item:
692
+ detail = f"{item['active_campaign_count']} active campaign(s)"
693
+ lines.append(
694
+ "| "
695
+ + " | ".join(
696
+ cell(item.get(k, ""))
697
+ for k in [
698
+ "code",
699
+ "level",
700
+ "list_id",
701
+ "prospect_id",
702
+ "node_id",
703
+ "field",
704
+ "template_field",
705
+ ]
706
+ )
707
+ + " | "
708
+ + cell(detail)
709
+ + " |"
710
+ )
711
+ if not report["findings"]:
712
+ lines += [
713
+ "",
714
+ "No findings from the checks performed. This is not a readiness certification.",
715
+ ]
716
+ lines += ["", "## Limits", ""] + ["- " + x for x in report["limits"]]
717
+ lines += [
718
+ "",
719
+ "Other saved actions were not evaluated: "
720
+ + ", ".join(cell(x["action_key"]) for x in report["unchecked_actions"])
721
+ + ".",
722
+ "",
723
+ ]
724
+ return "\n".join(lines)
725
+
726
+
727
+ def main():
728
+ parser = argparse.ArgumentParser(description=__doc__)
729
+ parser.add_argument("--input", type=Path, required=True)
730
+ parser.add_argument(
731
+ "--output-dir",
732
+ type=Path,
733
+ required=True,
734
+ help="New private directory; existing directories are not overwritten.",
735
+ )
736
+ args = parser.parse_args()
737
+ try:
738
+ snapshot = load_snapshot(args.input)
739
+ report = audit(snapshot)
740
+ args.output_dir.mkdir(mode=0o700, parents=False, exist_ok=False)
741
+ for name, content in [
742
+ (
743
+ "audit-report.json",
744
+ json.dumps(report, ensure_ascii=False, indent=2) + "\n",
745
+ ),
746
+ ("audit-report.md", markdown(report)),
747
+ ]:
748
+ path = args.output_dir / name
749
+ with path.open("x", encoding="utf-8") as handle:
750
+ os.chmod(path, 0o600)
751
+ handle.write(content)
752
+ print(
753
+ json.dumps(
754
+ {
755
+ "report_directory": str(args.output_dir),
756
+ "findings": len(report["findings"]),
757
+ "coverage_complete": report["all_selected_memberships_captured"],
758
+ }
759
+ )
760
+ )
761
+ except (InputError, OSError, json.JSONDecodeError) as error:
762
+ parser.exit(2, f"Audit could not run: {error}\n")
763
+
764
+
765
+ if __name__ == "__main__":
766
+ main()