millforge 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.
Files changed (116) hide show
  1. millforge/__init__.py +1174 -0
  2. millforge/_forge/LICENSE +21 -0
  3. millforge/_forge/PROVENANCE.json +295 -0
  4. millforge/_forge/UPDATE_POLICY.md +24 -0
  5. millforge/_forge/__init__.py +14 -0
  6. millforge/_forge/adapter.py +2232 -0
  7. millforge/_forge/base_runner.py +121 -0
  8. millforge/_forge/clients/__init__.py +10 -0
  9. millforge/_forge/clients/base.py +200 -0
  10. millforge/_forge/context/__init__.py +23 -0
  11. millforge/_forge/context/manager.py +178 -0
  12. millforge/_forge/context/strategies.py +335 -0
  13. millforge/_forge/core/__init__.py +16 -0
  14. millforge/_forge/core/inference.py +433 -0
  15. millforge/_forge/core/messages.py +119 -0
  16. millforge/_forge/core/runner.py +479 -0
  17. millforge/_forge/core/steps.py +108 -0
  18. millforge/_forge/core/workflow.py +400 -0
  19. millforge/_forge/errors.py +222 -0
  20. millforge/_forge/guardrails/__init__.py +21 -0
  21. millforge/_forge/guardrails/error_tracker.py +71 -0
  22. millforge/_forge/guardrails/guardrails.py +194 -0
  23. millforge/_forge/guardrails/nudge.py +47 -0
  24. millforge/_forge/guardrails/response_validator.py +119 -0
  25. millforge/_forge/guardrails/step_enforcer.py +183 -0
  26. millforge/_forge/prompts/__init__.py +16 -0
  27. millforge/_forge/prompts/nudges.py +95 -0
  28. millforge/_forge/prompts/templates.py +285 -0
  29. millforge/_version.py +3 -0
  30. millforge/artifacts.py +570 -0
  31. millforge/base/__init__.py +97 -0
  32. millforge/base/composition.py +402 -0
  33. millforge/base/context.py +285 -0
  34. millforge/base/harness.py +138 -0
  35. millforge/base/identity.py +465 -0
  36. millforge/base/options.py +34 -0
  37. millforge/base/platform.py +17 -0
  38. millforge/base/prompt.py +317 -0
  39. millforge/base/runner.py +546 -0
  40. millforge/compiled_plan.py +970 -0
  41. millforge/compiler/__init__.py +231 -0
  42. millforge/compiler/artifact_validation.py +257 -0
  43. millforge/compiler/canonicalization.py +169 -0
  44. millforge/compiler/capabilities.py +66 -0
  45. millforge/compiler/catalogs.py +500 -0
  46. millforge/compiler/diagnostics.py +491 -0
  47. millforge/compiler/graph.py +678 -0
  48. millforge/compiler/lowering.py +198 -0
  49. millforge/compiler/output.py +692 -0
  50. millforge/compiler/parsing.py +1424 -0
  51. millforge/compiler/requests.py +1180 -0
  52. millforge/compiler/schema_validation.py +272 -0
  53. millforge/compiler/semantic.py +490 -0
  54. millforge/compiler/service.py +448 -0
  55. millforge/compiler/source.py +375 -0
  56. millforge/compiler/validators.py +184 -0
  57. millforge/connectors/__init__.py +95 -0
  58. millforge/connectors/admission.py +801 -0
  59. millforge/connectors/broker.py +202 -0
  60. millforge/connectors/contracts.py +1159 -0
  61. millforge/connectors/diagnostics.py +189 -0
  62. millforge/connectors/fake.py +66 -0
  63. millforge/connectors/runtime.py +236 -0
  64. millforge/contracts.py +2860 -0
  65. millforge/custom_tools/__init__.py +67 -0
  66. millforge/custom_tools/compiler.py +724 -0
  67. millforge/custom_tools/contracts.py +1093 -0
  68. millforge/custom_tools/diagnostics.py +205 -0
  69. millforge/eval_artifacts.py +952 -0
  70. millforge/eval_boundary.py +2435 -0
  71. millforge/eval_fixtures/__init__.py +1 -0
  72. millforge/eval_fixtures/default_pack/__init__.py +1 -0
  73. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
  74. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
  75. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
  76. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
  77. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
  78. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
  79. millforge/eval_fixtures/default_pack/manifest.json +12 -0
  80. millforge/eval_modes.py +1282 -0
  81. millforge/eval_presets.py +1398 -0
  82. millforge/eval_reports.py +2517 -0
  83. millforge/eval_suite.py +2429 -0
  84. millforge/eval_trials.py +2632 -0
  85. millforge/eval_workflow.py +794 -0
  86. millforge/exceptions.py +122 -0
  87. millforge/model_backend.py +2098 -0
  88. millforge/protocols.py +340 -0
  89. millforge/py.typed +0 -0
  90. millforge/runtime.py +1791 -0
  91. millforge/testing/__init__.py +1089 -0
  92. millforge/tools/__init__.py +83 -0
  93. millforge/tools/builtin_runtime.py +1339 -0
  94. millforge/tools/builtins.py +773 -0
  95. millforge/tools/execution.py +1545 -0
  96. millforge/tools/path_policy.py +155 -0
  97. millforge/tools/pi_compat/PI_LICENSE +21 -0
  98. millforge/tools/pi_compat/PROVENANCE.json +55 -0
  99. millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
  100. millforge/tools/pi_compat/__init__.py +34 -0
  101. millforge/tools/pi_compat/contracts.py +49 -0
  102. millforge/tools/pi_compat/editing.py +390 -0
  103. millforge/tools/pi_compat/mutations.py +57 -0
  104. millforge/tools/pi_compat/operations.py +401 -0
  105. millforge/tools/pi_compat/paths.py +155 -0
  106. millforge/tools/pi_compat/process.py +1375 -0
  107. millforge/tools/pi_compat/search.py +738 -0
  108. millforge/tools/pi_compat/truncation.py +267 -0
  109. millforge/tools/pi_compat_catalog.py +396 -0
  110. millforge/tools/pi_compat_runtime.py +460 -0
  111. millforge/tools/registry.py +553 -0
  112. millforge/tools/results.py +533 -0
  113. millforge-0.1.0.dist-info/METADATA +844 -0
  114. millforge-0.1.0.dist-info/RECORD +116 -0
  115. millforge-0.1.0.dist-info/WHEEL +4 -0
  116. millforge-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,401 @@
1
+ """Synchronous filesystem operations in the Pi 0.79.6 compatibility pack."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from pathlib import Path
7
+
8
+ from .contracts import (
9
+ PiCompatErrorKind,
10
+ PiCompatOperationResult,
11
+ PiCompatSideEffectState,
12
+ )
13
+ from .mutations import file_mutation_lock
14
+ from .paths import (
15
+ _PathValidationError,
16
+ _is_absolute_cwd,
17
+ resolve_read_path,
18
+ resolve_to_cwd,
19
+ )
20
+ from .truncation import DEFAULT_MAX_BYTES, format_size, sanitize_text, truncate_head
21
+
22
+ __all__ = ["execute_ls", "execute_read", "execute_write"]
23
+
24
+ _DEFAULT_LS_LIMIT = 500
25
+ _PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
26
+
27
+
28
+ def _read_only_error(
29
+ message: str, error_kind: PiCompatErrorKind
30
+ ) -> PiCompatOperationResult:
31
+ return PiCompatOperationResult(
32
+ model_text=sanitize_text(message),
33
+ truncated=False,
34
+ error_kind=error_kind,
35
+ exit_code=None,
36
+ changed_path=None,
37
+ side_effect_state=PiCompatSideEffectState.NOT_ATTEMPTED,
38
+ )
39
+
40
+
41
+ def _invalid_arguments(message: str) -> PiCompatOperationResult:
42
+ return _read_only_error(message, PiCompatErrorKind.INVALID_ARGUMENTS)
43
+
44
+
45
+ def _has_embedded_nul(value: str) -> bool:
46
+ return "\x00" in value
47
+
48
+
49
+ def _cwd_error(cwd: object) -> str | None:
50
+ if not _is_absolute_cwd(cwd):
51
+ return "cwd must be an absolute path"
52
+ if "\x00" in str(cwd):
53
+ return "cwd must not contain NUL bytes"
54
+ return None
55
+
56
+
57
+ def _filesystem_error_kind(error: OSError) -> PiCompatErrorKind:
58
+ if isinstance(error, FileNotFoundError):
59
+ return PiCompatErrorKind.NOT_FOUND
60
+ if isinstance(error, PermissionError):
61
+ return PiCompatErrorKind.PERMISSION_DENIED
62
+ return PiCompatErrorKind.IO_ERROR
63
+
64
+
65
+ def _read_uint32_be(data: bytes, offset: int) -> int:
66
+ if offset + 4 > len(data):
67
+ return 0
68
+ return int.from_bytes(data[offset : offset + 4], "big")
69
+
70
+
71
+ def _is_animated_png(data: bytes) -> bool:
72
+ offset = len(_PNG_SIGNATURE)
73
+ while offset + 8 <= len(data):
74
+ chunk_length = _read_uint32_be(data, offset)
75
+ chunk_type_offset = offset + 4
76
+ if data[chunk_type_offset : chunk_type_offset + 4] == b"acTL":
77
+ return True
78
+ if data[chunk_type_offset : chunk_type_offset + 4] == b"IDAT":
79
+ return False
80
+ next_offset = offset + 8 + chunk_length + 4
81
+ if next_offset <= offset or next_offset > len(data):
82
+ return False
83
+ offset = next_offset
84
+ return False
85
+
86
+
87
+ def _supported_image_mime_type(data: bytes) -> str | None:
88
+ if data.startswith(b"\xff\xd8\xff"):
89
+ return None if len(data) > 3 and data[3] == 0xF7 else "image/jpeg"
90
+ if data.startswith(_PNG_SIGNATURE):
91
+ is_png = (
92
+ len(data) >= 16
93
+ and _read_uint32_be(data, len(_PNG_SIGNATURE)) == 13
94
+ and data[12:16] == b"IHDR"
95
+ )
96
+ if is_png and not _is_animated_png(data):
97
+ return "image/png"
98
+ return None
99
+ if data.startswith(b"GIF"):
100
+ return "image/gif"
101
+ if data.startswith(b"RIFF") and data[8:12] == b"WEBP":
102
+ return "image/webp"
103
+ return None
104
+
105
+
106
+ def _is_number(value: object) -> bool:
107
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
108
+
109
+
110
+ def _number_text(value: int | float) -> str:
111
+ if isinstance(value, float) and value.is_integer():
112
+ return str(int(value))
113
+ return str(value)
114
+
115
+
116
+ def _js_truthy_number(value: int | float) -> bool:
117
+ return not (value == 0 or (isinstance(value, float) and math.isnan(value)))
118
+
119
+
120
+ def _js_slice_index(value: int | float, length: int) -> int:
121
+ if isinstance(value, float):
122
+ if math.isnan(value):
123
+ return 0
124
+ if value == math.inf:
125
+ return length
126
+ if value == -math.inf:
127
+ return 0
128
+ integer = math.trunc(value)
129
+ if integer < 0:
130
+ return max(length + integer, 0)
131
+ return min(integer, length)
132
+
133
+
134
+ def _read_start_line(
135
+ offset: int | float | None, line_count: int
136
+ ) -> tuple[int | float, int]:
137
+ if offset is None or not _js_truthy_number(offset):
138
+ start_line: int | float = 0
139
+ else:
140
+ start_line = max(0, offset - 1)
141
+ return start_line, _js_slice_index(start_line, line_count)
142
+
143
+
144
+ def execute_read(
145
+ *,
146
+ cwd: Path,
147
+ path: str,
148
+ offset: int | float | None = None,
149
+ limit: int | float | None = None,
150
+ ) -> PiCompatOperationResult:
151
+ """Read text or a supported image using Pi's output wording and limits."""
152
+
153
+ cwd_error = _cwd_error(cwd)
154
+ if cwd_error is not None:
155
+ return _invalid_arguments(cwd_error)
156
+ if not isinstance(path, str):
157
+ return _invalid_arguments("path must be a string")
158
+ if _has_embedded_nul(path):
159
+ return _invalid_arguments("path must not contain NUL bytes")
160
+ if offset is not None and not _is_number(offset):
161
+ return _invalid_arguments("offset must be a number")
162
+ if limit is not None and not _is_number(limit):
163
+ return _invalid_arguments("limit must be a number")
164
+
165
+ try:
166
+ absolute_path = resolve_read_path(path, cwd)
167
+ data = absolute_path.read_bytes()
168
+ except _PathValidationError as error:
169
+ return _invalid_arguments(str(error))
170
+ except OSError as error:
171
+ return _read_only_error(str(error), _filesystem_error_kind(error))
172
+ except UnicodeError:
173
+ return _invalid_arguments("path cannot be encoded by this filesystem")
174
+
175
+ mime_type = _supported_image_mime_type(data[:4100])
176
+ if mime_type is not None:
177
+ return PiCompatOperationResult(
178
+ model_text=(
179
+ f"Read image file [{mime_type}]\n"
180
+ "[Image attachment omitted: Millforge's tool-result contract is "
181
+ f"text-only. File size: {len(data)} bytes.]"
182
+ ),
183
+ truncated=False,
184
+ error_kind=None,
185
+ exit_code=None,
186
+ changed_path=None,
187
+ side_effect_state=PiCompatSideEffectState.NOT_ATTEMPTED,
188
+ )
189
+
190
+ text_content = data.decode("utf-8", errors="replace")
191
+ all_lines = text_content.split("\n")
192
+ start_line, start_index = _read_start_line(offset, len(all_lines))
193
+ if start_line >= len(all_lines):
194
+ return _invalid_arguments(
195
+ "Offset "
196
+ f"{_number_text(offset if offset is not None else 0)} is beyond end "
197
+ f"of file ({len(all_lines)} lines total)"
198
+ )
199
+
200
+ user_limited_lines: int | float | None = None
201
+ if limit is None:
202
+ selected_content = "\n".join(all_lines[start_index:])
203
+ else:
204
+ end_line = min(start_line + limit, len(all_lines))
205
+ end_index = _js_slice_index(end_line, len(all_lines))
206
+ selected_content = "\n".join(all_lines[start_index:end_index])
207
+ user_limited_lines = end_line - start_line
208
+
209
+ truncation = truncate_head(selected_content)
210
+ start_line_display = start_line + 1
211
+ if truncation.first_line_exceeds_limit:
212
+ first_line = all_lines[start_index]
213
+ model_text = (
214
+ f"[Line {_number_text(start_line_display)} is "
215
+ f"{format_size(len(first_line.encode('utf-8')))}, exceeds "
216
+ f"{format_size(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n "
217
+ f"'{_number_text(start_line_display)}p' {path} | head -c "
218
+ f"{DEFAULT_MAX_BYTES}]"
219
+ )
220
+ elif truncation.truncated:
221
+ end_line_display = start_line_display + truncation.output_lines - 1
222
+ next_offset = end_line_display + 1
223
+ model_text = truncation.content
224
+ if truncation.truncated_by == "lines":
225
+ model_text += (
226
+ f"\n\n[Showing lines {_number_text(start_line_display)}-"
227
+ f"{_number_text(end_line_display)} of {len(all_lines)}. Use "
228
+ f"offset={_number_text(next_offset)} to continue.]"
229
+ )
230
+ else:
231
+ model_text += (
232
+ f"\n\n[Showing lines {_number_text(start_line_display)}-"
233
+ f"{_number_text(end_line_display)} of {len(all_lines)} "
234
+ f"({format_size(DEFAULT_MAX_BYTES)} limit). Use "
235
+ f"offset={_number_text(next_offset)} to continue.]"
236
+ )
237
+ elif user_limited_lines is not None and start_line + user_limited_lines < len(
238
+ all_lines
239
+ ):
240
+ remaining = len(all_lines) - (start_line + user_limited_lines)
241
+ next_offset = start_line + user_limited_lines + 1
242
+ model_text = (
243
+ f"{truncation.content}\n\n[{_number_text(remaining)} more lines in "
244
+ f"file. Use offset={_number_text(next_offset)} to continue.]"
245
+ )
246
+ else:
247
+ model_text = truncation.content
248
+
249
+ return PiCompatOperationResult(
250
+ model_text=sanitize_text(model_text),
251
+ truncated=truncation.truncated,
252
+ error_kind=None,
253
+ exit_code=None,
254
+ changed_path=None,
255
+ side_effect_state=PiCompatSideEffectState.NOT_ATTEMPTED,
256
+ )
257
+
258
+
259
+ def execute_ls(
260
+ *,
261
+ cwd: Path,
262
+ path: str | None = None,
263
+ limit: int | float | None = None,
264
+ ) -> PiCompatOperationResult:
265
+ """List Pi-compatible directory entries, including dotfiles and directories."""
266
+
267
+ cwd_error = _cwd_error(cwd)
268
+ if cwd_error is not None:
269
+ return _invalid_arguments(cwd_error)
270
+ if path is not None and not isinstance(path, str):
271
+ return _invalid_arguments("path must be a string")
272
+ if path is not None and _has_embedded_nul(path):
273
+ return _invalid_arguments("path must not contain NUL bytes")
274
+ if limit is not None and not _is_number(limit):
275
+ return _invalid_arguments("limit must be a number")
276
+
277
+ try:
278
+ dir_path = resolve_to_cwd(path or ".", cwd)
279
+ if not dir_path.exists():
280
+ return _read_only_error(
281
+ f"Path not found: {dir_path}", PiCompatErrorKind.NOT_FOUND
282
+ )
283
+ if not dir_path.is_dir():
284
+ return _invalid_arguments(f"Not a directory: {dir_path}")
285
+ entries = list(dir_path.iterdir())
286
+ except _PathValidationError as error:
287
+ return _invalid_arguments(str(error))
288
+ except OSError as error:
289
+ return _read_only_error(
290
+ f"Cannot read directory: {error}", _filesystem_error_kind(error)
291
+ )
292
+ except UnicodeError:
293
+ return _invalid_arguments("path cannot be encoded by this filesystem")
294
+
295
+ entries.sort(key=lambda entry: (entry.name.casefold(), entry.name))
296
+ effective_limit = _DEFAULT_LS_LIMIT if limit is None else limit
297
+ results: list[str] = []
298
+ entry_limit_reached = False
299
+ for entry in entries:
300
+ if len(results) >= effective_limit:
301
+ entry_limit_reached = True
302
+ break
303
+ try:
304
+ suffix = "/" if entry.is_dir() else ""
305
+ except OSError:
306
+ continue
307
+ results.append(f"{entry.name}{suffix}")
308
+
309
+ if not results:
310
+ return PiCompatOperationResult(
311
+ model_text="(empty directory)",
312
+ truncated=False,
313
+ error_kind=None,
314
+ exit_code=None,
315
+ changed_path=None,
316
+ side_effect_state=PiCompatSideEffectState.NOT_ATTEMPTED,
317
+ )
318
+
319
+ truncation = truncate_head("\n".join(results), max_lines=2**53 - 1)
320
+ notices: list[str] = []
321
+ if entry_limit_reached:
322
+ notices.append(
323
+ f"{_number_text(effective_limit)} entries limit reached. Use "
324
+ f"limit={_number_text(effective_limit * 2)} for more"
325
+ )
326
+ if truncation.truncated:
327
+ notices.append(f"{format_size(DEFAULT_MAX_BYTES)} limit reached")
328
+ model_text = truncation.content
329
+ if notices:
330
+ model_text += f"\n\n[{'. '.join(notices)}]"
331
+
332
+ return PiCompatOperationResult(
333
+ model_text=sanitize_text(model_text),
334
+ truncated=entry_limit_reached or truncation.truncated,
335
+ error_kind=None,
336
+ exit_code=None,
337
+ changed_path=None,
338
+ side_effect_state=PiCompatSideEffectState.NOT_ATTEMPTED,
339
+ )
340
+
341
+
342
+ def execute_write(
343
+ *,
344
+ cwd: Path,
345
+ path: str,
346
+ content: str,
347
+ ) -> PiCompatOperationResult:
348
+ """Create parent directories and overwrite one file under the mutation lock."""
349
+
350
+ cwd_error = _cwd_error(cwd)
351
+ if cwd_error is not None:
352
+ return _invalid_arguments(cwd_error)
353
+ if not isinstance(path, str):
354
+ return _invalid_arguments("path must be a string")
355
+ if _has_embedded_nul(path):
356
+ return _invalid_arguments("path must not contain NUL bytes")
357
+ if not isinstance(content, str):
358
+ return _invalid_arguments("content must be a string")
359
+
360
+ mutation_started = False
361
+ try:
362
+ absolute_path = resolve_to_cwd(path, cwd)
363
+ with file_mutation_lock(absolute_path):
364
+ try:
365
+ mutation_started = True
366
+ absolute_path.parent.mkdir(parents=True, exist_ok=True)
367
+ with absolute_path.open("w", encoding="utf-8", newline="") as output:
368
+ output.write(content)
369
+ except (OSError, UnicodeError) as error:
370
+ return PiCompatOperationResult(
371
+ model_text=sanitize_text(str(error)),
372
+ truncated=False,
373
+ error_kind=(
374
+ _filesystem_error_kind(error)
375
+ if isinstance(error, OSError)
376
+ else PiCompatErrorKind.IO_ERROR
377
+ ),
378
+ exit_code=None,
379
+ changed_path=None,
380
+ side_effect_state=(
381
+ PiCompatSideEffectState.COMPLETION_UNKNOWN
382
+ if mutation_started
383
+ else PiCompatSideEffectState.NOT_ATTEMPTED
384
+ ),
385
+ )
386
+ except _PathValidationError as error:
387
+ return _invalid_arguments(str(error))
388
+ except UnicodeError:
389
+ return _invalid_arguments("path cannot be encoded by this filesystem")
390
+
391
+ javascript_string_length = len(content.encode("utf-16-le")) // 2
392
+ return PiCompatOperationResult(
393
+ model_text=sanitize_text(
394
+ f"Successfully wrote {javascript_string_length} bytes to {path}"
395
+ ),
396
+ truncated=False,
397
+ error_kind=None,
398
+ exit_code=None,
399
+ changed_path=absolute_path,
400
+ side_effect_state=PiCompatSideEffectState.CONFIRMED_COMPLETE,
401
+ )
@@ -0,0 +1,155 @@
1
+ """Pi-compatible path normalization and read-path recovery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import unicodedata
8
+ from pathlib import Path
9
+ from urllib.parse import unquote, urlparse
10
+
11
+ __all__ = [
12
+ "expand_path",
13
+ "normalize_path",
14
+ "resolve_read_path",
15
+ "resolve_to_cwd",
16
+ ]
17
+
18
+ _NARROW_NO_BREAK_SPACE = "\u202f"
19
+ _UNICODE_SPACES = re.compile(r"[\u00a0\u2000-\u200a\u202f\u205f\u3000]")
20
+ _MACOS_AM_PM = re.compile(r" (AM|PM)\.", re.IGNORECASE)
21
+
22
+
23
+ class _PathValidationError(ValueError):
24
+ """Raised when a normalized path cannot be passed to this filesystem."""
25
+
26
+
27
+ def _is_absolute_cwd(cwd: object) -> bool:
28
+ return isinstance(cwd, Path) and cwd.is_absolute()
29
+
30
+
31
+ def _file_url_path(value: str) -> str:
32
+ parsed = urlparse(value)
33
+ decoded_path = unquote(parsed.path)
34
+ if os.name == "nt" and re.match(r"^/[A-Za-z]:", decoded_path):
35
+ decoded_path = decoded_path[1:]
36
+ if parsed.netloc and parsed.netloc != "localhost":
37
+ return f"//{parsed.netloc}{decoded_path}"
38
+ return decoded_path
39
+
40
+
41
+ def normalize_path(
42
+ input_path: str,
43
+ *,
44
+ trim: bool = False,
45
+ expand_tilde: bool = True,
46
+ home_directory: Path | None = None,
47
+ strip_at_prefix: bool = False,
48
+ normalize_unicode_spaces: bool = False,
49
+ ) -> str:
50
+ """Normalize a user path using the pinned Pi path utility's options."""
51
+
52
+ normalized = input_path.strip() if trim else input_path
53
+ if normalize_unicode_spaces:
54
+ normalized = _UNICODE_SPACES.sub(" ", normalized)
55
+ if strip_at_prefix and normalized.startswith("@"):
56
+ normalized = normalized[1:]
57
+
58
+ if expand_tilde:
59
+ home = str(home_directory if home_directory is not None else Path.home())
60
+ if normalized == "~":
61
+ return home
62
+ if normalized.startswith("~/") or (
63
+ os.name == "nt" and normalized.startswith("~\\")
64
+ ):
65
+ return os.path.join(home, normalized[2:])
66
+
67
+ if normalized.startswith("file://"):
68
+ return _file_url_path(normalized)
69
+ return normalized
70
+
71
+
72
+ def expand_path(input_path: str) -> str:
73
+ """Expand a Pi tool path without resolving it against a run cwd."""
74
+
75
+ return normalize_path(
76
+ input_path,
77
+ normalize_unicode_spaces=True,
78
+ strip_at_prefix=True,
79
+ )
80
+
81
+
82
+ def _validate_filesystem_path(value: str) -> None:
83
+ if "\x00" in value:
84
+ raise _PathValidationError("path must not contain NUL bytes")
85
+ try:
86
+ os.fsencode(value)
87
+ except UnicodeEncodeError as exc:
88
+ raise _PathValidationError("path cannot be encoded by this filesystem") from exc
89
+
90
+
91
+ def resolve_to_cwd(file_path: str, cwd: Path) -> Path:
92
+ """Resolve an absolute or run-relative Pi tool path without containment."""
93
+
94
+ _validate_filesystem_path(file_path)
95
+ try:
96
+ normalized = expand_path(file_path)
97
+ _validate_filesystem_path(normalized)
98
+ candidate = Path(normalized)
99
+ if not candidate.is_absolute():
100
+ candidate = cwd / candidate
101
+ resolved = Path(os.path.abspath(candidate))
102
+ _validate_filesystem_path(str(resolved))
103
+ except _PathValidationError:
104
+ raise
105
+ except (OSError, UnicodeError, ValueError) as exc:
106
+ raise _PathValidationError("path is invalid") from exc
107
+ return resolved
108
+
109
+
110
+ def _exists(path: Path) -> bool:
111
+ try:
112
+ return path.exists()
113
+ except (OSError, UnicodeError, ValueError):
114
+ return False
115
+
116
+
117
+ def _macos_screenshot_variant(path: Path) -> Path:
118
+ return Path(
119
+ _MACOS_AM_PM.sub(
120
+ lambda match: f"{_NARROW_NO_BREAK_SPACE}{match[1]}.", str(path)
121
+ )
122
+ )
123
+
124
+
125
+ def _nfd_variant(path: Path) -> Path:
126
+ return Path(unicodedata.normalize("NFD", str(path)))
127
+
128
+
129
+ def _curly_quote_variant(path: Path) -> Path:
130
+ return Path(str(path).replace("'", "\u2019"))
131
+
132
+
133
+ def resolve_read_path(file_path: str, cwd: Path) -> Path:
134
+ """Resolve a read path and try Pi's macOS filename recovery variants."""
135
+
136
+ resolved = resolve_to_cwd(file_path, cwd)
137
+ if _exists(resolved):
138
+ return resolved
139
+
140
+ am_pm_variant = _macos_screenshot_variant(resolved)
141
+ if am_pm_variant != resolved and _exists(am_pm_variant):
142
+ return am_pm_variant
143
+
144
+ nfd_variant = _nfd_variant(resolved)
145
+ if nfd_variant != resolved and _exists(nfd_variant):
146
+ return nfd_variant
147
+
148
+ curly_variant = _curly_quote_variant(resolved)
149
+ if curly_variant != resolved and _exists(curly_variant):
150
+ return curly_variant
151
+
152
+ nfd_curly_variant = _curly_quote_variant(nfd_variant)
153
+ if nfd_curly_variant != resolved and _exists(nfd_curly_variant):
154
+ return nfd_curly_variant
155
+ return resolved