glitchlings 0.2.5__cp312-cp312-win_amd64.whl → 0.9.3__cp312-cp312-win_amd64.whl

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 (85) hide show
  1. glitchlings/__init__.py +36 -17
  2. glitchlings/__main__.py +0 -1
  3. glitchlings/_zoo_rust/__init__.py +12 -0
  4. glitchlings/_zoo_rust.cp312-win_amd64.pyd +0 -0
  5. glitchlings/assets/__init__.py +180 -0
  6. glitchlings/assets/apostrofae_pairs.json +32 -0
  7. glitchlings/assets/ekkokin_homophones.json +2014 -0
  8. glitchlings/assets/hokey_assets.json +193 -0
  9. glitchlings/assets/lexemes/academic.json +1049 -0
  10. glitchlings/assets/lexemes/colors.json +1333 -0
  11. glitchlings/assets/lexemes/corporate.json +716 -0
  12. glitchlings/assets/lexemes/cyberpunk.json +22 -0
  13. glitchlings/assets/lexemes/lovecraftian.json +23 -0
  14. glitchlings/assets/lexemes/synonyms.json +3354 -0
  15. glitchlings/assets/mim1c_homoglyphs.json.gz.b64 +1064 -0
  16. glitchlings/assets/pipeline_assets.json +29 -0
  17. glitchlings/attack/__init__.py +53 -0
  18. glitchlings/attack/compose.py +299 -0
  19. glitchlings/attack/core.py +465 -0
  20. glitchlings/attack/encode.py +114 -0
  21. glitchlings/attack/metrics.py +104 -0
  22. glitchlings/attack/metrics_dispatch.py +70 -0
  23. glitchlings/attack/tokenization.py +157 -0
  24. glitchlings/auggie.py +283 -0
  25. glitchlings/compat/__init__.py +9 -0
  26. glitchlings/compat/loaders.py +355 -0
  27. glitchlings/compat/types.py +41 -0
  28. glitchlings/conf/__init__.py +41 -0
  29. glitchlings/conf/loaders.py +331 -0
  30. glitchlings/conf/schema.py +156 -0
  31. glitchlings/conf/types.py +72 -0
  32. glitchlings/config.toml +2 -0
  33. glitchlings/constants.py +59 -0
  34. glitchlings/dev/__init__.py +3 -0
  35. glitchlings/dev/docs.py +45 -0
  36. glitchlings/dlc/__init__.py +17 -3
  37. glitchlings/dlc/_shared.py +296 -0
  38. glitchlings/dlc/gutenberg.py +400 -0
  39. glitchlings/dlc/huggingface.py +37 -65
  40. glitchlings/dlc/prime.py +55 -114
  41. glitchlings/dlc/pytorch.py +98 -0
  42. glitchlings/dlc/pytorch_lightning.py +173 -0
  43. glitchlings/internal/__init__.py +16 -0
  44. glitchlings/internal/rust.py +159 -0
  45. glitchlings/internal/rust_ffi.py +432 -0
  46. glitchlings/main.py +123 -32
  47. glitchlings/runtime_config.py +24 -0
  48. glitchlings/util/__init__.py +29 -176
  49. glitchlings/util/adapters.py +65 -0
  50. glitchlings/util/keyboards.py +311 -0
  51. glitchlings/util/transcripts.py +108 -0
  52. glitchlings/zoo/__init__.py +47 -24
  53. glitchlings/zoo/assets/__init__.py +29 -0
  54. glitchlings/zoo/core.py +301 -167
  55. glitchlings/zoo/core_execution.py +98 -0
  56. glitchlings/zoo/core_planning.py +451 -0
  57. glitchlings/zoo/corrupt_dispatch.py +295 -0
  58. glitchlings/zoo/ekkokin.py +118 -0
  59. glitchlings/zoo/hokey.py +137 -0
  60. glitchlings/zoo/jargoyle.py +179 -274
  61. glitchlings/zoo/mim1c.py +106 -68
  62. glitchlings/zoo/pedant/__init__.py +107 -0
  63. glitchlings/zoo/pedant/core.py +105 -0
  64. glitchlings/zoo/pedant/forms.py +74 -0
  65. glitchlings/zoo/pedant/stones.py +74 -0
  66. glitchlings/zoo/redactyl.py +44 -175
  67. glitchlings/zoo/rng.py +259 -0
  68. glitchlings/zoo/rushmore.py +359 -116
  69. glitchlings/zoo/scannequin.py +18 -125
  70. glitchlings/zoo/transforms.py +386 -0
  71. glitchlings/zoo/typogre.py +76 -162
  72. glitchlings/zoo/validation.py +477 -0
  73. glitchlings/zoo/zeedub.py +33 -86
  74. glitchlings-0.9.3.dist-info/METADATA +334 -0
  75. glitchlings-0.9.3.dist-info/RECORD +80 -0
  76. {glitchlings-0.2.5.dist-info → glitchlings-0.9.3.dist-info}/entry_points.txt +1 -0
  77. glitchlings/zoo/_ocr_confusions.py +0 -34
  78. glitchlings/zoo/_rate.py +0 -21
  79. glitchlings/zoo/reduple.py +0 -169
  80. glitchlings-0.2.5.dist-info/METADATA +0 -490
  81. glitchlings-0.2.5.dist-info/RECORD +0 -27
  82. /glitchlings/{zoo → assets}/ocr_confusions.tsv +0 -0
  83. {glitchlings-0.2.5.dist-info → glitchlings-0.9.3.dist-info}/WHEEL +0 -0
  84. {glitchlings-0.2.5.dist-info → glitchlings-0.9.3.dist-info}/licenses/LICENSE +0 -0
  85. {glitchlings-0.2.5.dist-info → glitchlings-0.9.3.dist-info}/top_level.txt +0 -0
glitchlings/main.py CHANGED
@@ -4,15 +4,22 @@ from __future__ import annotations
4
4
 
5
5
  import argparse
6
6
  import difflib
7
- from pathlib import Path
7
+ import json
8
8
  import sys
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+ from typing import cast
12
+
13
+ import yaml
9
14
 
10
15
  from . import SAMPLE_TEXT
16
+ from .attack import Attack
17
+ from .conf import DEFAULT_ATTACK_SEED, build_gaggle, load_attack_config
11
18
  from .zoo import (
12
- Glitchling,
13
- Gaggle,
14
19
  BUILTIN_GLITCHLINGS,
15
20
  DEFAULT_GLITCHLING_NAMES,
21
+ Gaggle,
22
+ Glitchling,
16
23
  parse_glitchling_spec,
17
24
  summon,
18
25
  )
@@ -20,24 +27,30 @@ from .zoo import (
20
27
  MAX_NAME_WIDTH = max(len(glitchling.name) for glitchling in BUILTIN_GLITCHLINGS.values())
21
28
 
22
29
 
23
- def build_parser() -> argparse.ArgumentParser:
30
+ def build_parser(
31
+ *,
32
+ exit_on_error: bool = True,
33
+ include_text: bool = True,
34
+ ) -> argparse.ArgumentParser:
24
35
  """Create and configure the CLI argument parser.
25
36
 
26
37
  Returns:
27
38
  argparse.ArgumentParser: The configured argument parser instance.
28
- """
29
39
 
40
+ """
30
41
  parser = argparse.ArgumentParser(
31
42
  description=(
32
43
  "Summon glitchlings to corrupt text. Provide input text as an argument, "
33
44
  "via --file, or pipe it on stdin."
34
- )
35
- )
36
- parser.add_argument(
37
- "text",
38
- nargs="?",
39
- help="Text to corrupt. If omitted, stdin is used or --sample provides fallback text.",
45
+ ),
46
+ exit_on_error=exit_on_error,
40
47
  )
48
+ if include_text:
49
+ parser.add_argument(
50
+ "text",
51
+ nargs="*",
52
+ help="Text to corrupt. If omitted, stdin is used or --sample provides fallback text.",
53
+ )
41
54
  parser.add_argument(
42
55
  "-g",
43
56
  "--glitchling",
@@ -53,7 +66,7 @@ def build_parser() -> argparse.ArgumentParser:
53
66
  "-s",
54
67
  "--seed",
55
68
  type=int,
56
- default=151,
69
+ default=None,
57
70
  help="Seed controlling deterministic corruption order (default: 151).",
58
71
  )
59
72
  parser.add_argument(
@@ -77,12 +90,30 @@ def build_parser() -> argparse.ArgumentParser:
77
90
  action="store_true",
78
91
  help="List available glitchlings and exit.",
79
92
  )
93
+ parser.add_argument(
94
+ "-c",
95
+ "--config",
96
+ type=Path,
97
+ help="Load glitchlings from a YAML configuration file.",
98
+ )
99
+ parser.add_argument(
100
+ "--report",
101
+ "--attack",
102
+ dest="report_format",
103
+ nargs="?",
104
+ const="json",
105
+ choices=["json", "yaml", "yml"],
106
+ help=(
107
+ "Output a structured Attack report (default: json). Use --attack as an alias. "
108
+ "Includes tokens, token IDs, metrics, and counts."
109
+ ),
110
+ )
111
+
80
112
  return parser
81
113
 
82
114
 
83
115
  def list_glitchlings() -> None:
84
116
  """Print information about the available built-in glitchlings."""
85
-
86
117
  for key in DEFAULT_GLITCHLING_NAMES:
87
118
  glitchling = BUILTIN_GLITCHLINGS[key]
88
119
  display_name = glitchling.name
@@ -103,58 +134,83 @@ def read_text(args: argparse.Namespace, parser: argparse.ArgumentParser) -> str:
103
134
 
104
135
  Raises:
105
136
  SystemExit: Raised indirectly via ``parser.error`` on failure.
106
- """
107
137
 
108
- if args.file is not None:
138
+ """
139
+ file_path = cast(Path | None, getattr(args, "file", None))
140
+ if file_path is not None:
109
141
  try:
110
- return args.file.read_text(encoding="utf-8")
142
+ return file_path.read_text(encoding="utf-8")
111
143
  except OSError as exc:
112
- filename = getattr(exc, "filename", None) or args.file
144
+ filename = getattr(exc, "filename", None) or file_path
113
145
  reason = exc.strerror or str(exc)
114
146
  parser.error(f"Failed to read file {filename}: {reason}")
115
147
 
116
- if args.text:
117
- return args.text
148
+ text_argument = cast(str | list[str] | None, getattr(args, "text", None))
149
+ if isinstance(text_argument, list):
150
+ if text_argument:
151
+ return " ".join(text_argument)
152
+ text_argument = None
153
+ if isinstance(text_argument, str) and text_argument:
154
+ return text_argument
118
155
 
119
156
  if not sys.stdin.isatty():
120
157
  return sys.stdin.read()
121
158
 
122
- if args.sample:
159
+ if bool(getattr(args, "sample", False)):
123
160
  return SAMPLE_TEXT
124
161
 
125
162
  parser.error(
126
- "No input text provided. Supply text as an argument, use --file, pipe input, or pass --sample."
163
+ "No input text provided. Supply text as an argument, use --file, pipe input, or "
164
+ "pass --sample."
127
165
  )
128
166
  raise AssertionError("parser.error should exit")
129
167
 
130
168
 
131
169
  def summon_glitchlings(
132
- names: list[str] | None, parser: argparse.ArgumentParser, seed: int
170
+ names: list[str] | None,
171
+ parser: argparse.ArgumentParser,
172
+ seed: int | None,
173
+ *,
174
+ config_path: Path | None = None,
133
175
  ) -> Gaggle:
134
176
  """Instantiate the requested glitchlings and bundle them in a ``Gaggle``."""
177
+ if config_path is not None:
178
+ if names:
179
+ parser.error("Cannot combine --config with --glitchling.")
180
+ raise AssertionError("parser.error should exit")
135
181
 
182
+ try:
183
+ config = load_attack_config(config_path)
184
+ except (TypeError, ValueError) as exc:
185
+ parser.error(str(exc))
186
+ raise AssertionError("parser.error should exit")
187
+
188
+ return build_gaggle(config, seed_override=seed)
189
+
190
+ normalized: Sequence[str | Glitchling]
136
191
  if names:
137
- normalized: list[str | Glitchling] = []
192
+ parsed: list[str | Glitchling] = []
138
193
  for specification in names:
139
194
  try:
140
- normalized.append(parse_glitchling_spec(specification))
195
+ parsed.append(parse_glitchling_spec(specification))
141
196
  except ValueError as exc:
142
197
  parser.error(str(exc))
143
198
  raise AssertionError("parser.error should exit")
199
+ normalized = parsed
144
200
  else:
145
- normalized = DEFAULT_GLITCHLING_NAMES
201
+ normalized = list(DEFAULT_GLITCHLING_NAMES)
202
+
203
+ effective_seed = seed if seed is not None else DEFAULT_ATTACK_SEED
146
204
 
147
205
  try:
148
- return summon(normalized, seed=seed)
206
+ return summon(list(normalized), seed=effective_seed)
149
207
  except ValueError as exc:
150
208
  parser.error(str(exc))
151
209
  raise AssertionError("parser.error should exit")
152
210
 
153
211
 
154
-
155
212
  def show_diff(original: str, corrupted: str) -> None:
156
213
  """Display a unified diff between the original and corrupted text."""
157
-
158
214
  diff_lines = list(
159
215
  difflib.unified_diff(
160
216
  original.splitlines(keepends=True),
@@ -180,16 +236,46 @@ def run_cli(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
180
236
 
181
237
  Returns:
182
238
  int: Exit code for the process (``0`` on success).
183
- """
184
239
 
240
+ """
185
241
  if args.list:
186
242
  list_glitchlings()
187
243
  return 0
188
244
 
245
+ report_format = cast(str | None, getattr(args, "report_format", None))
246
+ if report_format and args.diff:
247
+ parser.error("--diff cannot be combined with --report/--attack output.")
248
+ raise AssertionError("parser.error should exit")
249
+
250
+ normalized_report_format = None
251
+ if report_format:
252
+ normalized_report_format = "yaml" if report_format == "yml" else report_format
253
+
189
254
  text = read_text(args, parser)
190
- gaggle = summon_glitchlings(args.glitchlings, parser, args.seed)
255
+ gaggle = summon_glitchlings(
256
+ args.glitchlings,
257
+ parser,
258
+ args.seed,
259
+ config_path=args.config,
260
+ )
261
+
262
+ if normalized_report_format:
263
+ attack_seed = args.seed if args.seed is not None else getattr(gaggle, "seed", None)
264
+ attack = Attack(gaggle, seed=attack_seed)
265
+ result = attack.run(text)
266
+ payload = result.to_report()
267
+ payload["summary"] = result.summary()
268
+
269
+ if normalized_report_format == "json":
270
+ print(json.dumps(payload, indent=2))
271
+ else:
272
+ print(yaml.safe_dump(payload, sort_keys=False))
273
+ return 0
191
274
 
192
- corrupted = gaggle(text)
275
+ corrupted = gaggle.corrupt(text)
276
+ if not isinstance(corrupted, str):
277
+ message = "Gaggle returned non-string output for string input"
278
+ raise TypeError(message)
193
279
 
194
280
  if args.diff:
195
281
  show_diff(text, corrupted)
@@ -207,10 +293,15 @@ def main(argv: list[str] | None = None) -> int:
207
293
 
208
294
  Returns:
209
295
  int: Exit code suitable for use with ``sys.exit``.
296
+
210
297
  """
298
+ if argv is None:
299
+ raw_args = sys.argv[1:]
300
+ else:
301
+ raw_args = list(argv)
211
302
 
212
303
  parser = build_parser()
213
- args = parser.parse_args(argv)
304
+ args = parser.parse_args(raw_args)
214
305
  return run_cli(args, parser)
215
306
 
216
307
 
@@ -0,0 +1,24 @@
1
+ """Compatibility wrapper for runtime configuration helpers.
2
+
3
+ Prefer ``glitchlings.conf`` for imports.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .conf import (
9
+ CONFIG_ENV_VAR,
10
+ DEFAULT_CONFIG_PATH,
11
+ RuntimeConfig,
12
+ get_config,
13
+ reload_config,
14
+ reset_config,
15
+ )
16
+
17
+ __all__ = [
18
+ "CONFIG_ENV_VAR",
19
+ "DEFAULT_CONFIG_PATH",
20
+ "RuntimeConfig",
21
+ "get_config",
22
+ "reload_config",
23
+ "reset_config",
24
+ ]
@@ -1,181 +1,34 @@
1
- import difflib
2
- from collections.abc import Iterable
3
-
4
- SAMPLE_TEXT = "One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, waved about helplessly as he looked."
5
-
6
-
7
- def string_diffs(a: str, b: str) -> list[list[tuple[str, str, str]]]:
8
- """
9
- Compare two strings using SequenceMatcher and return
10
- grouped adjacent opcodes (excluding 'equal' tags).
11
-
12
- Each element is a tuple: (tag, a_text, b_text).
13
- """
14
- sm = difflib.SequenceMatcher(None, a, b)
15
- ops: list[list[tuple[str, str, str]]] = []
16
- buffer: list[tuple[str, str, str]] = []
17
-
18
- for tag, i1, i2, j1, j2 in sm.get_opcodes():
19
- if tag == "equal":
20
- # flush any buffered operations before skipping
21
- if buffer:
22
- ops.append(buffer)
23
- buffer = []
24
- continue
25
-
26
- # append operation to buffer
27
- buffer.append((tag, a[i1:i2], b[j1:j2]))
28
-
29
- # flush trailing buffer
30
- if buffer:
31
- ops.append(buffer)
32
-
33
- return ops
34
-
35
-
36
- KeyNeighborMap = dict[str, list[str]]
37
- KeyboardLayouts = dict[str, KeyNeighborMap]
38
-
39
-
40
- def _build_neighbor_map(rows: Iterable[str]) -> KeyNeighborMap:
41
- """Derive 8-neighbour adjacency lists from keyboard layout rows."""
42
-
43
- grid: dict[tuple[int, int], str] = {}
44
- for y, row in enumerate(rows):
45
- for x, char in enumerate(row):
46
- if char == " ":
47
- continue
48
- grid[(x, y)] = char.lower()
49
-
50
- neighbors: KeyNeighborMap = {}
51
- for (x, y), char in grid.items():
52
- seen: list[str] = []
53
- for dy in (-1, 0, 1):
54
- for dx in (-1, 0, 1):
55
- if dx == 0 and dy == 0:
56
- continue
57
- candidate = grid.get((x + dx, y + dy))
58
- if candidate is None:
59
- continue
60
- seen.append(candidate)
61
- # Preserve encounter order but drop duplicates for determinism
62
- deduped = list(dict.fromkeys(seen))
63
- neighbors[char] = deduped
64
-
65
- return neighbors
66
-
67
-
68
- _KEYNEIGHBORS: KeyboardLayouts = {
69
- "CURATOR_QWERTY": {
70
- "a": [*"qwsz"],
71
- "b": [*"vghn "],
72
- "c": [*"xdfv "],
73
- "d": [*"serfcx"],
74
- "e": [*"wsdrf34"],
75
- "f": [*"drtgvc"],
76
- "g": [*"ftyhbv"],
77
- "h": [*"gyujnb"],
78
- "i": [*"ujko89"],
79
- "j": [*"huikmn"],
80
- "k": [*"jilom,"],
81
- "l": [*"kop;.,"],
82
- "m": [*"njk, "],
83
- "n": [*"bhjm "],
84
- "o": [*"iklp90"],
85
- "p": [*"o0-[;l"],
86
- "q": [*"was 12"],
87
- "r": [*"edft45"],
88
- "s": [*"awedxz"],
89
- "t": [*"r56ygf"],
90
- "u": [*"y78ijh"],
91
- "v": [*"cfgb "],
92
- "w": [*"q23esa"],
93
- "x": [*"zsdc "],
94
- "y": [*"t67uhg"],
95
- "z": [*"asx"],
96
- }
97
- }
98
-
99
-
100
- def _register_layout(name: str, rows: Iterable[str]) -> None:
101
- _KEYNEIGHBORS[name] = _build_neighbor_map(rows)
102
-
103
-
104
- _register_layout(
105
- "DVORAK",
106
- (
107
- "`1234567890[]\\",
108
- " ',.pyfgcrl/=\\",
109
- " aoeuidhtns-",
110
- " ;qjkxbmwvz",
111
- ),
112
- )
113
-
114
- _register_layout(
115
- "COLEMAK",
116
- (
117
- "`1234567890-=",
118
- " qwfpgjluy;[]\\",
119
- " arstdhneio'",
120
- " zxcvbkm,./",
121
- ),
122
- )
123
-
124
- _register_layout(
125
- "QWERTY",
126
- (
127
- "`1234567890-=",
128
- " qwertyuiop[]\\",
129
- " asdfghjkl;'",
130
- " zxcvbnm,./",
131
- ),
1
+ from glitchlings.zoo.transforms import KeyNeighborMap
2
+ from glitchlings.zoo.transforms import (
3
+ compute_string_diffs as string_diffs,
132
4
  )
133
5
 
134
- _register_layout(
135
- "AZERTY",
136
- (
137
- "²&é\"'(-è_çà)=",
138
- " azertyuiop^$",
139
- " qsdfghjklmù*",
140
- " <wxcvbn,;:!",
141
- ),
6
+ from .keyboards import (
7
+ KEYNEIGHBORS,
8
+ SHIFT_MAPS,
9
+ KeyboardLayouts,
10
+ KeyNeighbors,
11
+ ShiftMap,
12
+ ShiftMaps,
142
13
  )
143
14
 
144
- _register_layout(
145
- "QWERTZ",
146
- (
147
- "^1234567890ß´",
148
- " qwertzuiopü+",
149
- " asdfghjklöä#",
150
- " yxcvbnm,.-",
151
- ),
15
+ __all__ = [
16
+ "SAMPLE_TEXT",
17
+ "string_diffs",
18
+ "KeyNeighborMap",
19
+ "KeyboardLayouts",
20
+ "ShiftMap",
21
+ "ShiftMaps",
22
+ "KeyNeighbors",
23
+ "KEYNEIGHBORS",
24
+ "SHIFT_MAPS",
25
+ ]
26
+
27
+ SAMPLE_TEXT = (
28
+ "One morning, when Gregor Samsa woke from troubled dreams, he found himself "
29
+ "transformed in his bed into a horrible vermin. He lay on his armour-like back, and "
30
+ "if he lifted his head a little he could see his brown belly, slightly domed and "
31
+ "divided by arches into stiff sections. The bedding was hardly able to cover it and "
32
+ "seemed ready to slide off any moment. His many legs, pitifully thin compared with "
33
+ "the size of the rest of him, waved about helplessly as he looked."
152
34
  )
153
-
154
- _register_layout(
155
- "SPANISH_QWERTY",
156
- (
157
- "º1234567890'¡",
158
- " qwertyuiop´+",
159
- " asdfghjklñ´",
160
- " <zxcvbnm,.-",
161
- ),
162
- )
163
-
164
- _register_layout(
165
- "SWEDISH_QWERTY",
166
- (
167
- "§1234567890+´",
168
- " qwertyuiopå¨",
169
- " asdfghjklöä'",
170
- " <zxcvbnm,.-",
171
- ),
172
- )
173
-
174
-
175
- class KeyNeighbors:
176
- def __init__(self) -> None:
177
- for layout_name, layout in _KEYNEIGHBORS.items():
178
- setattr(self, layout_name, layout)
179
-
180
-
181
- KEYNEIGHBORS: KeyNeighbors = KeyNeighbors()
@@ -0,0 +1,65 @@
1
+ """Adapter helpers shared across Python and DLC integrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+ from ..zoo import Gaggle, Glitchling, summon
9
+ from .transcripts import TranscriptTarget
10
+
11
+
12
+ def coerce_gaggle(
13
+ glitchlings: Glitchling | Gaggle | str | Iterable[str | Glitchling],
14
+ *,
15
+ seed: int,
16
+ apply_seed_to_existing: bool = False,
17
+ transcript_target: TranscriptTarget | None = None,
18
+ ) -> Gaggle:
19
+ """Return a :class:`Gaggle` built from any supported glitchling specifier.
20
+
21
+ Args:
22
+ glitchlings: A single Glitchling, Gaggle, string specification, or iterable
23
+ of glitchlings/specs.
24
+ seed: Seed to use when constructing a new Gaggle from the input.
25
+ apply_seed_to_existing: When True, also apply the seed to an existing
26
+ Gaggle instance. When False (default), existing Gaggles keep their
27
+ current seed.
28
+ transcript_target: Which transcript turns to corrupt. When None (default),
29
+ uses the Gaggle default ("last"). Accepts:
30
+ - "last": corrupt only the last turn (default)
31
+ - "all": corrupt all turns
32
+ - "assistant": corrupt only assistant turns
33
+ - "user": corrupt only user turns
34
+ - int: corrupt a specific index (negative indexing supported)
35
+ - Sequence[int]: corrupt specific indices
36
+ """
37
+ if isinstance(glitchlings, Gaggle):
38
+ if apply_seed_to_existing:
39
+ glitchlings.seed = seed
40
+ glitchlings.sort_glitchlings()
41
+ if transcript_target is not None:
42
+ glitchlings.transcript_target = transcript_target
43
+ return glitchlings
44
+
45
+ if isinstance(glitchlings, (Glitchling, str)):
46
+ resolved: Iterable[Any] = [glitchlings]
47
+ else:
48
+ resolved = glitchlings
49
+
50
+ # Validate entries before passing to summon to give better error messages
51
+ resolved_list = list(resolved)
52
+ for index, entry in enumerate(resolved_list):
53
+ if not isinstance(entry, (str, Glitchling)):
54
+ raise TypeError(
55
+ f"glitchlings sequence entries must be Glitchling instances "
56
+ f"or string specifications (index {index})"
57
+ )
58
+
59
+ gaggle = summon(resolved_list, seed=seed)
60
+ if transcript_target is not None:
61
+ gaggle.transcript_target = transcript_target
62
+ return gaggle
63
+
64
+
65
+ __all__ = ["coerce_gaggle"]