moveset 0.1.0__py3-none-any.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.
moveset/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Immutable linear pipelines over explicitly selected capability catalogs."""
2
+
3
+ from .core import BoundPipeline, DefinitionError, Input, Pipeline, PipelineError, Ref, Result, Step, StepError
4
+
5
+ __all__ = ["BoundPipeline", "DefinitionError", "Input", "Pipeline", "PipelineError", "Ref", "Result", "Step", "StepError"]
moveset/_validation.py ADDED
@@ -0,0 +1,121 @@
1
+ """Fallback JSON validation when hidden_moves does not export validate_json_value."""
2
+ from __future__ import annotations
3
+ import math
4
+ from collections.abc import Mapping
5
+ from typing import Any
6
+ from hidden_moves.adapters.errors import CapabilityExposureError
7
+
8
+ _KEYWORDS = frozenset({
9
+ "$schema", "title", "description", "default", "examples", "type", "enum",
10
+ "anyOf", "properties", "required", "additionalProperties", "items",
11
+ })
12
+ _TYPES = frozenset({"null", "boolean", "integer", "number", "string", "array", "object"})
13
+
14
+ class ValueValidationError(ValueError):
15
+ """A finite JSON value does not match the declared subset."""
16
+
17
+ def check_schema(schema: Mapping[str, Any]) -> None:
18
+ if not isinstance(schema, Mapping):
19
+ raise CapabilityExposureError("Schema nodes must be objects.")
20
+ unknown = set(schema) - _KEYWORDS
21
+ if unknown:
22
+ raise CapabilityExposureError(f"Unsupported schema keywords: {", ".join(sorted(unknown))}.")
23
+ if "type" in schema:
24
+ types = schema["type"]
25
+ if isinstance(types, str):
26
+ types = (types,)
27
+ if not isinstance(types, (list, tuple)) or not types or any(
28
+ not isinstance(item, str) or item not in _TYPES for item in types
29
+ ):
30
+ raise CapabilityExposureError("Schema type must be a supported JSON type or nonempty type list.")
31
+ if "enum" in schema and (not isinstance(schema["enum"], (list, tuple)) or not schema["enum"]):
32
+ raise CapabilityExposureError("Schema enum must be a nonempty array.")
33
+ if "anyOf" in schema:
34
+ branches = schema["anyOf"]
35
+ if not isinstance(branches, (list, tuple)) or not branches:
36
+ raise CapabilityExposureError("Schema anyOf must be a nonempty array.")
37
+ for branch in branches:
38
+ check_schema(branch)
39
+ properties = schema.get("properties", {})
40
+ if not isinstance(properties, Mapping):
41
+ raise CapabilityExposureError("Schema properties must be an object.")
42
+ for child in properties.values():
43
+ check_schema(child)
44
+ if "required" in schema:
45
+ required = schema["required"]
46
+ if not isinstance(required, (list, tuple)) or any(not isinstance(item, str) for item in required):
47
+ raise CapabilityExposureError("Schema required must be an array of property names.")
48
+ if len(required) != len(set(required)):
49
+ raise CapabilityExposureError("Schema required names must be unique.")
50
+ additional = schema.get("additionalProperties", True)
51
+ if not isinstance(additional, bool):
52
+ check_schema(additional)
53
+ if additional is False and any(name not in properties for name in schema.get("required", ())):
54
+ raise CapabilityExposureError("A closed schema requires a property it does not declare.")
55
+ if "items" in schema:
56
+ check_schema(schema["items"])
57
+
58
+ def _matches(value: Any, kind: str) -> bool:
59
+ if kind == "null":
60
+ return value is None
61
+ if kind == "boolean":
62
+ return isinstance(value, bool)
63
+ if kind == "integer":
64
+ return type(value) is int or (type(value) is float and math.isfinite(value) and value.is_integer())
65
+ if kind == "number":
66
+ return type(value) is int or (type(value) is float and math.isfinite(value))
67
+ return isinstance(value, {"string": str, "array": list, "object": dict}[kind])
68
+
69
+ def _equal(first: Any, second: Any) -> bool:
70
+ if isinstance(first, bool) != isinstance(second, bool):
71
+ return False
72
+ if isinstance(first, Mapping) and isinstance(second, Mapping):
73
+ return first.keys() == second.keys() and all(_equal(first[key], second[key]) for key in first)
74
+ if isinstance(first, (tuple, list)) and isinstance(second, (tuple, list)):
75
+ return len(first) == len(second) and all(_equal(a, b) for a, b in zip(first, second))
76
+ return first == second
77
+
78
+ def validate_value(schema: Mapping[str, Any], value: Any, path: str = "$arguments") -> None:
79
+ if "type" in schema:
80
+ types = schema["type"]
81
+ types = (types,) if isinstance(types, str) else types
82
+ if not any(_matches(value, kind) for kind in types):
83
+ raise ValueValidationError(f"{path}: expected {" or ".join(types)}.")
84
+ if "enum" in schema and not any(_equal(value, item) for item in schema["enum"]):
85
+ raise ValueValidationError(f"{path}: value is outside the declared enum.")
86
+ if "anyOf" in schema:
87
+ for branch in schema["anyOf"]:
88
+ try:
89
+ validate_value(branch, value, path)
90
+ break
91
+ except ValueValidationError:
92
+ pass
93
+ else:
94
+ raise ValueValidationError(f"{path}: value does not match any declared type.")
95
+ if isinstance(value, dict):
96
+ properties = schema.get("properties", {})
97
+ for name in schema.get("required", ()):
98
+ if name not in value:
99
+ raise ValueValidationError(f"{path}.{name}: required argument is missing.")
100
+ additional = schema.get("additionalProperties", True)
101
+ for name, item in value.items():
102
+ if name in properties:
103
+ validate_value(properties[name], item, f"{path}.{name}")
104
+ elif additional is False:
105
+ raise ValueValidationError(f"{path}.{name}: unexpected argument.")
106
+ elif isinstance(additional, Mapping):
107
+ validate_value(additional, item, f"{path}.{name}")
108
+ if isinstance(value, list) and "items" in schema:
109
+ for index, item in enumerate(value):
110
+ validate_value(schema["items"], item, f"{path}[{index}]")
111
+
112
+ def fallback_validate_json_value(schema: Mapping[str, Any], value: Any, path: str = "$value") -> Any:
113
+ from hidden_moves import MoveSchemaError
114
+ from hidden_moves.core.schema import json_value
115
+ check_schema(schema)
116
+ try:
117
+ copied = json_value(value)
118
+ except (MoveSchemaError, RecursionError) as error:
119
+ raise ValueValidationError(f"{path}: {error}") from error
120
+ validate_value(schema, copied, path)
121
+ return copied
moveset/core.py ADDED
@@ -0,0 +1,380 @@
1
+ """Explicit data mappings; capability and lifecycle behavior belongs to their owners."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import hashlib
7
+ import inspect
8
+ import json
9
+ import math
10
+ import re
11
+ from typing import Any
12
+
13
+ from hidden_moves.adapters import CapabilityCatalog
14
+ try:
15
+ from hidden_moves.adapters import validate_json_value
16
+ except ImportError:
17
+ from ._validation import fallback_validate_json_value as validate_json_value
18
+
19
+ MAX_BYTES = 1_048_576
20
+ MAX_DEPTH = 32
21
+ MAX_STEPS = 256
22
+
23
+
24
+ class PipelineError(ValueError):
25
+ """A definition or value cannot be used by this pipeline."""
26
+
27
+
28
+ class DefinitionError(PipelineError):
29
+ """A passive definition is invalid."""
30
+
31
+
32
+ class StepError(PipelineError):
33
+ """A selected operation or one of its mappings failed."""
34
+
35
+ def __init__(self, step: Step, message: str):
36
+ self.step_id = step.id
37
+ self.capability = step.capability
38
+ super().__init__(f"step {step.id!r} ({step.capability}): {message}")
39
+
40
+
41
+ def identifier(value: str) -> str:
42
+ if type(value) is not str or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", value):
43
+ raise DefinitionError("Identifiers require 1–128 ASCII letters, digits, dots, underscores or hyphens")
44
+ return value
45
+
46
+
47
+ def canonical(value: Any) -> bytes:
48
+ try:
49
+ data = json.dumps(value, ensure_ascii=False, allow_nan=False, sort_keys=True,
50
+ separators=(",", ":")).encode("utf-8")
51
+ except (TypeError, ValueError, UnicodeError, RecursionError) as error:
52
+ raise PipelineError("Expected finite UTF-8 JSON") from error
53
+ if len(data) > MAX_BYTES:
54
+ raise PipelineError(f"JSON exceeds {MAX_BYTES} bytes")
55
+ return data
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class Input:
60
+ name: str
61
+ path: tuple[str | int, ...] = ()
62
+
63
+ def __post_init__(self):
64
+ identifier(self.name)
65
+ _path(self.path)
66
+
67
+ def __getitem__(self, part: str | int) -> Input:
68
+ return Input(self.name, (*self.path, part))
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class Ref:
73
+ step: str
74
+ path: tuple[str | int, ...] = ()
75
+
76
+ def __post_init__(self):
77
+ identifier(self.step)
78
+ _path(self.path)
79
+
80
+ def __getitem__(self, part: str | int) -> Ref:
81
+ return Ref(self.step, (*self.path, part))
82
+
83
+
84
+ def _path(path):
85
+ if type(path) is not tuple or len(path) > MAX_DEPTH or any(
86
+ type(part) not in (str, int) or (type(part) is int and part < 0) for part in path
87
+ ):
88
+ raise DefinitionError("Paths must be bounded tuples of string keys or nonnegative indexes")
89
+ canonical(list(path))
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class _Object:
94
+ items: tuple
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class _Array:
99
+ items: tuple
100
+
101
+
102
+ def freeze(value, depth=0, *, expressions=True):
103
+ if depth > MAX_DEPTH:
104
+ raise DefinitionError(f"Values exceed depth {MAX_DEPTH}")
105
+ if expressions and isinstance(value, (Input, Ref)):
106
+ return value
107
+ if type(value) is dict:
108
+ if any(type(key) is not str for key in value):
109
+ raise DefinitionError("Object keys must be strings")
110
+ return _Object(tuple((key, freeze(item, depth + 1, expressions=expressions)) for key, item in value.items()))
111
+ if type(value) is list:
112
+ return _Array(tuple(freeze(item, depth + 1, expressions=expressions) for item in value))
113
+ if value is None or type(value) in (str, bool, int) or (type(value) is float and math.isfinite(value)):
114
+ canonical(value)
115
+ return value
116
+ raise DefinitionError("Values must be finite JSON or explicit Input/Ref expressions")
117
+
118
+
119
+ def thaw(value):
120
+ if isinstance(value, _Object):
121
+ return {key: thaw(item) for key, item in value.items}
122
+ if isinstance(value, _Array):
123
+ return [thaw(item) for item in value.items]
124
+ return value
125
+
126
+
127
+ def copy_json(value):
128
+ copied = thaw(freeze(value, expressions=False))
129
+ canonical(copied)
130
+ return copied
131
+
132
+
133
+ def expression(value):
134
+ if isinstance(value, Input):
135
+ return {"input": {"name": value.name, "path": list(value.path)}}
136
+ if isinstance(value, Ref):
137
+ return {"ref": {"step": value.step, "path": list(value.path)}}
138
+ if isinstance(value, _Object):
139
+ return {"object": {key: expression(item) for key, item in value.items}}
140
+ if isinstance(value, _Array):
141
+ return {"array": [expression(item) for item in value.items]}
142
+ return {"value": value}
143
+
144
+
145
+ def references(value):
146
+ if isinstance(value, (Ref, Input)):
147
+ yield value
148
+ elif isinstance(value, (_Object, _Array)):
149
+ for item in (item for _, item in value.items) if isinstance(value, _Object) else value.items:
150
+ yield from references(item)
151
+
152
+
153
+ def resolve(value, inputs, results):
154
+ if isinstance(value, (Input, Ref)):
155
+ selected = inputs if isinstance(value, Input) else results
156
+ key = value.name if isinstance(value, Input) else value.step
157
+ try:
158
+ selected = selected[key]
159
+ for part in value.path:
160
+ if (type(part) is str and type(selected) is dict) or (type(part) is int and type(selected) is list):
161
+ selected = selected[part]
162
+ else:
163
+ raise KeyError(part)
164
+ except (KeyError, IndexError) as error:
165
+ raise PipelineError(f"Cannot resolve {expression(value)}") from error
166
+ return copy_json(selected)
167
+ if isinstance(value, _Object):
168
+ return {key: resolve(item, inputs, results) for key, item in value.items}
169
+ if isinstance(value, _Array):
170
+ return [resolve(item, inputs, results) for item in value.items]
171
+ return value
172
+
173
+
174
+ @dataclass(frozen=True, init=False)
175
+ class Step:
176
+ id: str
177
+ capability: str
178
+ _arguments: _Object
179
+
180
+ def __init__(self, id: str, capability: str, arguments: dict):
181
+ identifier(id)
182
+ if type(capability) is not str or not capability or len(capability) > 512:
183
+ raise DefinitionError("A step requires a bounded capability name")
184
+ if type(arguments) is not dict:
185
+ raise DefinitionError("Step arguments must be an object")
186
+ object.__setattr__(self, "id", id)
187
+ object.__setattr__(self, "capability", capability)
188
+ object.__setattr__(self, "_arguments", freeze(arguments))
189
+ canonical(expression(self._arguments))
190
+
191
+ @property
192
+ def arguments(self):
193
+ return thaw(self._arguments)
194
+
195
+
196
+ @dataclass(frozen=True, init=False)
197
+ class Pipeline:
198
+ name: str
199
+ version: str
200
+ steps: tuple[Step, ...]
201
+ _outputs: _Object
202
+
203
+ def __init__(self, name: str, *, version: str, steps: tuple[Step, ...], outputs: dict):
204
+ identifier(name)
205
+ identifier(version)
206
+ if type(steps) is not tuple or not 1 <= len(steps) <= MAX_STEPS or any(type(step) is not Step for step in steps):
207
+ raise DefinitionError(f"A pipeline requires a tuple of 1–{MAX_STEPS} Steps")
208
+ previous = set()
209
+ for step in steps:
210
+ if step.id in previous:
211
+ raise DefinitionError(f"Duplicate step {step.id!r}")
212
+ for ref in references(step._arguments):
213
+ if isinstance(ref, Ref) and ref.step not in previous:
214
+ raise DefinitionError(f"step {step.id!r}: reference must name an earlier step: {ref.step!r}")
215
+ previous.add(step.id)
216
+ if type(outputs) is not dict:
217
+ raise DefinitionError("Outputs must be an explicit object")
218
+ frozen = freeze(outputs)
219
+ if any(isinstance(ref, Ref) and ref.step not in previous for ref in references(frozen)):
220
+ raise DefinitionError("Output reference names an unknown step")
221
+ for key, value in (("name", name), ("version", version), ("steps", steps), ("_outputs", frozen)):
222
+ object.__setattr__(self, key, value)
223
+ canonical(self.manifest)
224
+
225
+ @property
226
+ def outputs(self):
227
+ return thaw(self._outputs)
228
+
229
+ @property
230
+ def manifest(self):
231
+ return {"schema": 1, "name": self.name, "version": self.version,
232
+ "steps": [{"id": step.id, "capability": step.capability,
233
+ "arguments": expression(step._arguments)} for step in self.steps],
234
+ "outputs": expression(self._outputs)}
235
+
236
+ def bind(self, catalog: CapabilityCatalog, *, catalog_version: str) -> BoundPipeline:
237
+ return BoundPipeline(self, catalog, catalog_version)
238
+
239
+
240
+ def _partial(schema, value, inputs=None):
241
+ """Validate known portions; unknown producer values are checked at invocation."""
242
+ if isinstance(value, Ref):
243
+ return
244
+ if isinstance(value, Input):
245
+ if inputs is not None:
246
+ validate_json_value(schema, resolve(value, inputs, {}))
247
+ return
248
+ if "anyOf" in schema:
249
+ for branch in schema["anyOf"]:
250
+ try:
251
+ _partial(branch, value, inputs)
252
+ break
253
+ except (ValueError, TypeError):
254
+ pass
255
+ else:
256
+ raise DefinitionError("Mapping does not match any declared type")
257
+ if isinstance(value, _Object):
258
+ values = dict(value.items)
259
+ if "type" in schema:
260
+ validate_json_value({"type": schema["type"]}, {})
261
+ if any(name not in values for name in schema.get("required", ())):
262
+ raise DefinitionError("A required argument is missing")
263
+ properties = schema.get("properties", {})
264
+ additional = schema.get("additionalProperties", True)
265
+ for key, item in value.items:
266
+ if key not in properties and additional is False:
267
+ raise DefinitionError(f"Unexpected argument {key!r}")
268
+ _partial(properties.get(key, additional if isinstance(additional, dict) else {}), item, inputs)
269
+ elif isinstance(value, _Array):
270
+ if "type" in schema:
271
+ validate_json_value({"type": schema["type"]}, [])
272
+ for item in value.items:
273
+ _partial(schema.get("items", {}), item, inputs)
274
+ else:
275
+ validate_json_value(schema, value)
276
+ if not tuple(references(value)):
277
+ validate_json_value(schema, thaw(value))
278
+
279
+
280
+ @dataclass(frozen=True)
281
+ class Result:
282
+ """A run's caller-owned JSON snapshots."""
283
+
284
+ results: dict
285
+ outputs: dict
286
+
287
+
288
+ @dataclass(frozen=True, init=False)
289
+ class BoundPipeline:
290
+ definition: Pipeline
291
+ catalog: CapabilityCatalog
292
+ catalog_version: str
293
+
294
+ def __init__(self, definition, catalog, catalog_version):
295
+ if not isinstance(catalog, CapabilityCatalog):
296
+ raise DefinitionError("Bind an explicitly selected CapabilityCatalog")
297
+ identifier(catalog_version)
298
+ for step in definition.steps:
299
+ try:
300
+ schema = _schema_json(catalog.describe(step.capability).input_schema)
301
+ _partial(schema, step._arguments)
302
+ except Exception as error:
303
+ raise StepError(step, str(error)) from error
304
+ object.__setattr__(self, "definition", definition)
305
+ object.__setattr__(self, "catalog", catalog)
306
+ object.__setattr__(self, "catalog_version", catalog_version)
307
+ canonical(self.manifest)
308
+
309
+ @property
310
+ def manifest(self):
311
+ names = sorted({step.capability for step in self.definition.steps})
312
+ return {"definition": self.definition.manifest, "catalog_version": self.catalog_version,
313
+ "capabilities": [{"name": name, "input": _schema_json(self.catalog.describe(name).input_schema),
314
+ "output": _schema_json(self.catalog.describe(name).output_schema),
315
+ "async": self.catalog.describe(name).is_async} for name in names]}
316
+
317
+ @property
318
+ def fingerprint(self):
319
+ return hashlib.sha256(canonical(self.manifest)).hexdigest()
320
+
321
+ def prepare(self, inputs: dict):
322
+ if type(inputs) is not dict:
323
+ raise PipelineError("Run inputs must be a JSON object")
324
+ inputs = copy_json(inputs)
325
+ for step in self.definition.steps:
326
+ try:
327
+ _partial(_schema_json(self.catalog.describe(step.capability).input_schema), step._arguments, inputs)
328
+ except Exception as error:
329
+ raise StepError(step, str(error)) from error
330
+ for ref in references(self.definition._outputs):
331
+ if isinstance(ref, Input):
332
+ resolve(ref, inputs, {})
333
+ return inputs
334
+
335
+ def finish(self, inputs, results):
336
+ return Result(copy_json(results), resolve(self.definition._outputs, inputs, results))
337
+
338
+ def _invoke(self, step, inputs, results):
339
+ return self.catalog.invoke(step.capability, resolve(step._arguments, inputs, results))
340
+
341
+ def run(self, inputs: dict) -> Result:
342
+ inputs = self.prepare(inputs)
343
+ if any(self.catalog.describe(step.capability).is_async for step in self.definition.steps):
344
+ raise DefinitionError("Pipeline contains async capabilities; use arun()")
345
+ results = {}
346
+ for step in self.definition.steps:
347
+ try:
348
+ result = self._invoke(step, inputs, results)
349
+ if inspect.isawaitable(result):
350
+ if inspect.iscoroutine(result):
351
+ result.close()
352
+ raise DefinitionError("Capability returned an awaitable; use arun()")
353
+ results[step.id] = copy_json(self.catalog.serialize_result(step.capability, result))
354
+ except Exception as error:
355
+ raise StepError(step, str(error)) from error
356
+ return self.finish(inputs, results)
357
+
358
+ async def arun(self, inputs: dict) -> Result:
359
+ inputs = self.prepare(inputs)
360
+ results = {}
361
+ for step in self.definition.steps:
362
+ try:
363
+ result = self._invoke(step, inputs, results)
364
+ if inspect.isawaitable(result):
365
+ result = await result
366
+ results[step.id] = copy_json(self.catalog.serialize_result(step.capability, result))
367
+ except Exception as error:
368
+ raise StepError(step, str(error)) from error
369
+ return self.finish(inputs, results)
370
+
371
+
372
+ def _schema_json(schema):
373
+ """Catalog metadata uses frozen mappings/tuples; normalize without changing values."""
374
+ from collections.abc import Mapping
375
+
376
+ if isinstance(schema, Mapping):
377
+ return {key: _schema_json(item) for key, item in schema.items()}
378
+ if isinstance(schema, (list, tuple)):
379
+ return [_schema_json(item) for item in schema]
380
+ return schema
moveset/py.typed ADDED
File without changes
moveset/sequence.py ADDED
@@ -0,0 +1,70 @@
1
+ """Optional compilation into func_seq's public Script and lifecycle contract."""
2
+
3
+ import sys
4
+
5
+ if sys.version_info < (3, 14):
6
+ raise ImportError("The moveset sequence adapter requires Python 3.14 or newer")
7
+
8
+ import hashlib
9
+ import inspect
10
+
11
+ from func_seq import Ref as SequenceRef, Script
12
+
13
+ from .core import BoundPipeline, DefinitionError, Input, Ref, StepError, _Array, _Object, canonical, copy_json, resolve
14
+
15
+
16
+ def _mapping(value, inputs):
17
+ if isinstance(value, Input):
18
+ return resolve(value, inputs, {})
19
+ if isinstance(value, Ref):
20
+ return SequenceRef(value.step, value.path)
21
+ if isinstance(value, _Object):
22
+ return {key: _mapping(item, inputs) for key, item in value.items}
23
+ if isinstance(value, _Array):
24
+ return [_mapping(item, inputs) for item in value.items]
25
+ return value
26
+
27
+
28
+ def _wrapper(bound, step, asynchronous):
29
+ def invoke(arguments):
30
+ try:
31
+ result = bound.catalog.invoke(step.capability, arguments)
32
+ if inspect.isawaitable(result):
33
+ if inspect.iscoroutine(result):
34
+ result.close()
35
+ raise DefinitionError("Capability returned an awaitable; compile with asynchronous=True")
36
+ return copy_json(bound.catalog.serialize_result(step.capability, result))
37
+ except Exception as error:
38
+ raise StepError(step, str(error)) from error
39
+
40
+ async def ainvoke(arguments):
41
+ try:
42
+ result = bound.catalog.invoke(step.capability, arguments)
43
+ if inspect.isawaitable(result):
44
+ result = await result
45
+ return copy_json(bound.catalog.serialize_result(step.capability, result))
46
+ except Exception as error:
47
+ raise StepError(step, str(error)) from error
48
+
49
+ return ainvoke if asynchronous else invoke
50
+
51
+
52
+ def compile_script(bound: BoundPipeline, inputs: dict, *, asynchronous: bool = False) -> Script:
53
+ """Compile validated mappings; return func_seq's executor without wrapping state.
54
+
55
+ Use asynchronous=True with Script.arun for mixed/awaitable capabilities. The
56
+ execution mode is part of compatibility identity. Control, pace, observers,
57
+ checkpoint, retry and restart are passed directly to Script.run/arun.
58
+ """
59
+ if type(asynchronous) is not bool:
60
+ raise DefinitionError("asynchronous must be a boolean")
61
+ inputs = bound.prepare(inputs)
62
+ if not asynchronous and any(bound.catalog.describe(step.capability).is_async for step in bound.definition.steps):
63
+ raise DefinitionError("Async capabilities require asynchronous=True and Script.arun")
64
+ version = hashlib.sha256(canonical({"pipeline": bound.fingerprint, "inputs": inputs,
65
+ "asynchronous": asynchronous})).hexdigest()
66
+ script = Script(bound.definition.name, version=version)
67
+ for step in bound.definition.steps:
68
+ script.add_function(_wrapper(bound, step, asynchronous),
69
+ _mapping(step._arguments, inputs), step_id=step.id)
70
+ return script
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: moveset
3
+ Version: 0.1.0
4
+ Summary: Explicit linear composition of selected Python capabilities.
5
+ Author: ellainng
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Dist: hidden-moves>=0.1.0
9
+ Requires-Dist: func-seq>=0.1.0 ; python_full_version >= '3.14' and extra == 'sequence'
10
+ Requires-Python: >=3.11
11
+ Project-URL: Source, https://github.com/wispsy/moveset
12
+ Project-URL: Issues, https://github.com/wispsy/moveset/issues
13
+ Project-URL: Project, https://github.com/orgs/wispsy/projects/23
14
+ Provides-Extra: sequence
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Moveset
18
+
19
+ Explicit linear composition of selected Python capabilities. Python 3.11+.
20
+
21
+ ```python
22
+ from hidden_moves import Moves
23
+ from hidden_moves.adapters import CapabilityCatalog
24
+ from moveset import Input, Pipeline, Ref, Step
25
+
26
+ def normalize(text: str) -> str:
27
+ return " ".join(text.lower().split())
28
+
29
+ moves = Moves()
30
+ moves.learn(normalize)
31
+ selected = CapabilityCatalog(moves, ["normalize"])
32
+ definition = Pipeline("example", version="1", steps=(
33
+ Step("clean", "normalize", {"text": Input("text")}),
34
+ ), outputs={"text": Ref("clean")})
35
+ bound = definition.bind(selected, catalog_version="text-v1")
36
+ assert bound.run({"text": " Hello WORLD "}).outputs == {"text": "hello world"}
37
+ ```
38
+
39
+ Definitions snapshot nested arguments and outputs. `Input("payload")["key"][0]`
40
+ selects run data; `Ref("clean")` selects an earlier result. Mappings are explicit:
41
+ scalar results are never automatically wrapped in objects. Binding validates
42
+ selected capability names and known argument shapes. Run preflight validates all
43
+ known inputs before effects; dynamic result paths and values are checked before
44
+ the dependent call. Hidden Moves owns its documented schema subset and callable
45
+ binding. No provider is activated by creating or executing a pipeline.
46
+
47
+ `arun(inputs)` awaits mixed sync/async steps in order. `run` rejects declared async
48
+ steps before executing anything. Cancellation propagates; ordinary runs start
49
+ fresh. Results are caller-owned finite JSON snapshots, bounded to 1 MiB with depth
50
+ 32; definitions have at most 256 steps. StepError identifies the step/capability
51
+ and retains the original cause. There is no retry, persistence or concurrency in
52
+ the core.
53
+
54
+ The bound fingerprint includes mappings, schemas, async flags and catalog version.
55
+ Change `catalog_version` when callable behavior, resources, defaults or configuration
56
+ change; this is not source authentication. The optional lifecycle adapter belongs
57
+ in the `sequence` profile and delegates durable execution to func_seq.
58
+
59
+ This source development profile pins Hidden Moves to an immutable revision. It is
60
+ independently built and versioned; release requirements and publication are tracked
61
+ separately in the [Wispsy Composition Project](https://github.com/orgs/wispsy/projects/23).
62
+
63
+ Build with `uv build --out-dir dist`; `python scripts/qualify.py dist` installs the
64
+ wheel and source archive outside the checkout and runs the contract tests. CI
65
+ qualifies Python 3.11 on Linux/Windows and Python 3.14 on Linux/macOS/Windows.
@@ -0,0 +1,9 @@
1
+ moveset/__init__.py,sha256=AH6aFF9jWipBHXtSOcp3JHLF8oKbPDVdhx930vaJ7Ew,315
2
+ moveset/_validation.py,sha256=3r22tH89xnQ0vDvRGjFdnkXM7sQDzW9_MEU9vT137Wo,5411
3
+ moveset/core.py,sha256=5MPFH6r4NDQN0iCx6Gt0XAKKrtnt292zHkcxSrPoMHY,14358
4
+ moveset/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ moveset/sequence.py,sha256=w4qbFJUcHM777HUeii_nn6VOBGhqHVTivtw_e9Vs--Y,3026
6
+ moveset-0.1.0.dist-info/licenses/LICENSE,sha256=MJiaArfnOql8NJRA2TI9JIYDOoiis3Rh8xQruzBRXoU,1066
7
+ moveset-0.1.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
8
+ moveset-0.1.0.dist-info/METADATA,sha256=QfzzOicr_Gl7ya2hht2orVkDU7u-Xjs1J8WPpJ02PEw,3057
9
+ moveset-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.7
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ella Inng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.