packwright 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.
- packwright/__init__.py +3 -0
- packwright/__main__.py +5 -0
- packwright/adapters/__init__.py +10 -0
- packwright/adapters/claude_code.py +377 -0
- packwright/adapters/codex.py +311 -0
- packwright/adapters/cursor.py +375 -0
- packwright/checker/__init__.py +3 -0
- packwright/checker/scoring.py +924 -0
- packwright/cli.py +971 -0
- packwright/core/__init__.py +57 -0
- packwright/core/adapter_layout.py +35 -0
- packwright/core/adopt.py +199 -0
- packwright/core/character_intake.py +1649 -0
- packwright/core/emotion_engine_contract.py +147 -0
- packwright/core/errors.py +13 -0
- packwright/core/handoff.py +531 -0
- packwright/core/install.py +2114 -0
- packwright/core/intake_contract.py +105 -0
- packwright/core/knowledge_contract.py +212 -0
- packwright/core/loader.py +35 -0
- packwright/core/memory_projection.py +126 -0
- packwright/core/naming.py +98 -0
- packwright/core/pack_metadata.py +100 -0
- packwright/core/path_safety.py +70 -0
- packwright/core/resolver.py +66 -0
- packwright/core/validation.py +645 -0
- packwright/core/workspace_contract.py +102 -0
- packwright-0.1.0.dist-info/METADATA +213 -0
- packwright-0.1.0.dist-info/RECORD +33 -0
- packwright-0.1.0.dist-info/WHEEL +5 -0
- packwright-0.1.0.dist-info/entry_points.txt +2 -0
- packwright-0.1.0.dist-info/licenses/LICENSE +21 -0
- packwright-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .errors import PackwrightValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_relative_path(value, label="path"):
|
|
8
|
+
"""Return a normalized relative path or reject an unsafe path value."""
|
|
9
|
+
if not isinstance(value, str) or not value.strip():
|
|
10
|
+
raise PackwrightValidationError([f"{label} must be a non-empty relative path"])
|
|
11
|
+
if "\x00" in value:
|
|
12
|
+
raise PackwrightValidationError([f"{label} contains a null byte"])
|
|
13
|
+
path = Path(value)
|
|
14
|
+
if path.is_absolute() or path == Path(".") or ".." in path.parts:
|
|
15
|
+
raise PackwrightValidationError([f"{label} must be relative and stay inside its root: {value}"])
|
|
16
|
+
return path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_source_path(root, value, label="source path", require_file=True):
|
|
20
|
+
"""Resolve a readable path and require its final target to stay under root."""
|
|
21
|
+
relative = validate_relative_path(value, label)
|
|
22
|
+
resolved_root = Path(root).resolve()
|
|
23
|
+
candidate = resolved_root / relative
|
|
24
|
+
try:
|
|
25
|
+
resolved = candidate.resolve(strict=True)
|
|
26
|
+
resolved.relative_to(resolved_root)
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
raise PackwrightValidationError([f"{label} does not exist: {value}"])
|
|
29
|
+
except (OSError, ValueError):
|
|
30
|
+
raise PackwrightValidationError([f"{label} escapes its root: {value}"])
|
|
31
|
+
if require_file and not resolved.is_file():
|
|
32
|
+
raise PackwrightValidationError([f"{label} is not a file: {value}"])
|
|
33
|
+
return resolved
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_destination_path(root, value, label="destination path"):
|
|
37
|
+
"""Resolve a write destination without allowing traversal through symlinks."""
|
|
38
|
+
relative = validate_relative_path(value, label)
|
|
39
|
+
resolved_root = Path(root).resolve()
|
|
40
|
+
candidate = resolved_root / relative
|
|
41
|
+
current = resolved_root
|
|
42
|
+
for part in relative.parts:
|
|
43
|
+
current = current / part
|
|
44
|
+
if current.is_symlink():
|
|
45
|
+
raise PackwrightValidationError([f"{label} traverses a symlink: {value}"])
|
|
46
|
+
try:
|
|
47
|
+
candidate.resolve(strict=False).relative_to(resolved_root)
|
|
48
|
+
except (OSError, ValueError):
|
|
49
|
+
raise PackwrightValidationError([f"{label} escapes its root: {value}"])
|
|
50
|
+
return candidate
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def resolve_mechanism_file(mechanism, value, label="referenced file"):
|
|
54
|
+
source = mechanism.get("source", {}) if isinstance(mechanism, Mapping) else {}
|
|
55
|
+
base_dir = source.get("base_dir") if isinstance(source, Mapping) else None
|
|
56
|
+
if not base_dir:
|
|
57
|
+
raise PackwrightValidationError([f"{label} has no mechanism source root"])
|
|
58
|
+
roots = [base_dir]
|
|
59
|
+
fallback_roots = source.get("fallback_roots", []) if isinstance(source, Mapping) else []
|
|
60
|
+
if isinstance(fallback_roots, list):
|
|
61
|
+
roots.extend(root for root in fallback_roots if isinstance(root, str) and root)
|
|
62
|
+
missing = []
|
|
63
|
+
for root in roots:
|
|
64
|
+
try:
|
|
65
|
+
return resolve_source_path(root, value, label)
|
|
66
|
+
except PackwrightValidationError as exc:
|
|
67
|
+
if any("escapes its root" in issue or "must be relative" in issue for issue in exc.issues):
|
|
68
|
+
raise
|
|
69
|
+
missing.extend(exc.issues)
|
|
70
|
+
raise PackwrightValidationError(missing[:1] or [f"{label} does not exist: {value}"])
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import re
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
|
|
5
|
+
from .errors import PackwrightValidationError
|
|
6
|
+
from .validation import validate_mechanism
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PLACEHOLDER_PATTERN = re.compile(r"{{\s*([A-Za-z0-9_.-]+)\s*}}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_mechanism(data, parameters=None):
|
|
13
|
+
"""Validate and resolve a character mechanism spec using parameter overrides."""
|
|
14
|
+
validate_mechanism(data)
|
|
15
|
+
values = _resolve_parameter_values(data.get("parameters", {}), parameters or {})
|
|
16
|
+
resolved = _replace_placeholders(copy.deepcopy(data), values)
|
|
17
|
+
resolved["resolved_parameters"] = copy.deepcopy(values)
|
|
18
|
+
validate_mechanism(resolved)
|
|
19
|
+
return resolved
|
|
20
|
+
def _resolve_parameter_values(parameter_specs, overrides):
|
|
21
|
+
issues = []
|
|
22
|
+
values = {}
|
|
23
|
+
|
|
24
|
+
for name in overrides:
|
|
25
|
+
if name not in parameter_specs:
|
|
26
|
+
issues.append(f"unknown parameter override: {name}")
|
|
27
|
+
|
|
28
|
+
for name, spec in parameter_specs.items():
|
|
29
|
+
if name in overrides:
|
|
30
|
+
values[name] = overrides[name]
|
|
31
|
+
elif "default" in spec:
|
|
32
|
+
values[name] = spec["default"]
|
|
33
|
+
elif spec.get("required"):
|
|
34
|
+
issues.append(f"missing required parameter: {name}")
|
|
35
|
+
else:
|
|
36
|
+
values[name] = ""
|
|
37
|
+
|
|
38
|
+
if issues:
|
|
39
|
+
raise PackwrightValidationError(issues)
|
|
40
|
+
return values
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _replace_placeholders(value, parameters):
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
return _replace_string(value, parameters)
|
|
46
|
+
if isinstance(value, Mapping):
|
|
47
|
+
return {key: _replace_placeholders(item, parameters) for key, item in value.items()}
|
|
48
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
49
|
+
return [_replace_placeholders(item, parameters) for item in value]
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _replace_string(value, parameters):
|
|
54
|
+
issues = []
|
|
55
|
+
|
|
56
|
+
def replace(match):
|
|
57
|
+
name = match.group(1)
|
|
58
|
+
if name not in parameters:
|
|
59
|
+
issues.append(f"unknown placeholder: {name}")
|
|
60
|
+
return match.group(0)
|
|
61
|
+
return str(parameters[name])
|
|
62
|
+
|
|
63
|
+
rendered = PLACEHOLDER_PATTERN.sub(replace, value)
|
|
64
|
+
if issues:
|
|
65
|
+
raise PackwrightValidationError(issues)
|
|
66
|
+
return rendered
|