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,390 @@
1
+ """Pi-derived exact and fuzzy multi-edit behavior."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno
6
+ import unicodedata
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import cast
11
+
12
+ from .contracts import (
13
+ PiCompatErrorKind,
14
+ PiCompatOperationResult,
15
+ PiCompatSideEffectState,
16
+ )
17
+ from .mutations import file_mutation_lock
18
+ from .paths import _PathValidationError, _is_absolute_cwd, resolve_to_cwd
19
+ from .truncation import sanitize_text
20
+
21
+ __all__ = [
22
+ "Edit",
23
+ "PiCompatEditError",
24
+ "apply_edits_to_normalized_content",
25
+ "detect_line_ending",
26
+ "execute_edit",
27
+ "normalize_for_fuzzy_match",
28
+ "normalize_to_lf",
29
+ "restore_line_endings",
30
+ "strip_bom",
31
+ ]
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class Edit:
36
+ """One Pi edit replacement."""
37
+
38
+ old_text: str
39
+ new_text: str
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class _MatchedEdit:
44
+ edit_index: int
45
+ match_index: int
46
+ match_length: int
47
+ new_text: str
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class _Match:
52
+ found: bool
53
+ index: int
54
+ length: int
55
+ used_fuzzy_match: bool
56
+
57
+
58
+ class PiCompatEditError(ValueError):
59
+ """A Pi-compatible edit validation conflict."""
60
+
61
+
62
+ _FUZZY_TRANSLATION_TABLE = str.maketrans(
63
+ cast(
64
+ dict[str, str | int | None],
65
+ {
66
+ "\u2010": "-",
67
+ "\u2011": "-",
68
+ "\u2012": "-",
69
+ "\u2013": "-",
70
+ "\u2014": "-",
71
+ "\u2015": "-",
72
+ "\u2212": "-",
73
+ "\u00a0": " ",
74
+ "\u2002": " ",
75
+ "\u2003": " ",
76
+ "\u2004": " ",
77
+ "\u2005": " ",
78
+ "\u2006": " ",
79
+ "\u2007": " ",
80
+ "\u2008": " ",
81
+ "\u2009": " ",
82
+ "\u200a": " ",
83
+ "\u202f": " ",
84
+ "\u205f": " ",
85
+ "\u3000": " ",
86
+ },
87
+ )
88
+ )
89
+
90
+
91
+ def detect_line_ending(content: str) -> str:
92
+ """Return Pi's detected output line ending."""
93
+
94
+ crlf_index = content.find("\r\n")
95
+ lf_index = content.find("\n")
96
+ if lf_index == -1 or crlf_index == -1:
97
+ return "\n"
98
+ return "\r\n" if crlf_index < lf_index else "\n"
99
+
100
+
101
+ def normalize_to_lf(text: str) -> str:
102
+ """Normalize all supported newline spellings to LF."""
103
+
104
+ return text.replace("\r\n", "\n").replace("\r", "\n")
105
+
106
+
107
+ def restore_line_endings(text: str, ending: str) -> str:
108
+ """Restore Pi's detected CRLF convention after applying edits."""
109
+
110
+ return text.replace("\n", "\r\n") if ending == "\r\n" else text
111
+
112
+
113
+ def strip_bom(content: str) -> tuple[str, str]:
114
+ """Split a UTF-8 BOM from the text that the model matches."""
115
+
116
+ if content.startswith("\ufeff"):
117
+ return "\ufeff", content[1:]
118
+ return "", content
119
+
120
+
121
+ def normalize_for_fuzzy_match(text: str) -> str:
122
+ """Apply Pi's progressive Unicode and trailing-whitespace normalization."""
123
+
124
+ normalized = unicodedata.normalize("NFKC", text)
125
+ normalized = "\n".join(line.rstrip() for line in normalized.split("\n"))
126
+ return (
127
+ normalized.replace("\u2018", "'")
128
+ .replace("\u2019", "'")
129
+ .replace("\u201a", "'")
130
+ .replace("\u201b", "'")
131
+ .replace("\u201c", '"')
132
+ .replace("\u201d", '"')
133
+ .replace("\u201e", '"')
134
+ .replace("\u201f", '"')
135
+ .translate(_FUZZY_TRANSLATION_TABLE)
136
+ )
137
+
138
+
139
+ def _fuzzy_find_text(content: str, old_text: str) -> _Match:
140
+ exact_index = content.find(old_text)
141
+ if exact_index != -1:
142
+ return _Match(True, exact_index, len(old_text), False)
143
+
144
+ fuzzy_content = normalize_for_fuzzy_match(content)
145
+ fuzzy_old_text = normalize_for_fuzzy_match(old_text)
146
+ fuzzy_index = fuzzy_content.find(fuzzy_old_text)
147
+ if fuzzy_index == -1:
148
+ return _Match(False, -1, 0, False)
149
+ return _Match(True, fuzzy_index, len(fuzzy_old_text), True)
150
+
151
+
152
+ def _count_occurrences(content: str, old_text: str) -> int:
153
+ return normalize_for_fuzzy_match(content).count(normalize_for_fuzzy_match(old_text))
154
+
155
+
156
+ def _not_found_error(path: str, edit_index: int, total_edits: int) -> PiCompatEditError:
157
+ if total_edits == 1:
158
+ return PiCompatEditError(
159
+ f"Could not find the exact text in {path}. The old text must match "
160
+ "exactly including all whitespace and newlines."
161
+ )
162
+ return PiCompatEditError(
163
+ f"Could not find edits[{edit_index}] in {path}. The oldText must match "
164
+ "exactly including all whitespace and newlines."
165
+ )
166
+
167
+
168
+ def _duplicate_error(
169
+ path: str, edit_index: int, total_edits: int, occurrences: int
170
+ ) -> PiCompatEditError:
171
+ if total_edits == 1:
172
+ return PiCompatEditError(
173
+ f"Found {occurrences} occurrences of the text in {path}. The text "
174
+ "must be unique. Please provide more context to make it unique."
175
+ )
176
+ return PiCompatEditError(
177
+ f"Found {occurrences} occurrences of edits[{edit_index}] in {path}. "
178
+ "Each oldText must be unique. Please provide more context to make it unique."
179
+ )
180
+
181
+
182
+ def _empty_old_text_error(
183
+ path: str, edit_index: int, total_edits: int
184
+ ) -> PiCompatEditError:
185
+ if total_edits == 1:
186
+ return PiCompatEditError(f"oldText must not be empty in {path}.")
187
+ return PiCompatEditError(
188
+ f"edits[{edit_index}].oldText must not be empty in {path}."
189
+ )
190
+
191
+
192
+ def _no_change_error(path: str, total_edits: int) -> PiCompatEditError:
193
+ if total_edits == 1:
194
+ return PiCompatEditError(
195
+ f"No changes made to {path}. The replacement produced identical content. "
196
+ "This might indicate an issue with special characters or the text not "
197
+ "existing as expected."
198
+ )
199
+ return PiCompatEditError(
200
+ f"No changes made to {path}. The replacements produced identical content."
201
+ )
202
+
203
+
204
+ def apply_edits_to_normalized_content(
205
+ normalized_content: str, edits: Sequence[Edit], path: str
206
+ ) -> tuple[str, str]:
207
+ """Match all Pi edits against one original LF-normalized content snapshot."""
208
+
209
+ normalized_edits = [
210
+ Edit(normalize_to_lf(edit.old_text), normalize_to_lf(edit.new_text))
211
+ for edit in edits
212
+ ]
213
+ for index, edit in enumerate(normalized_edits):
214
+ if not edit.old_text:
215
+ raise _empty_old_text_error(path, index, len(normalized_edits))
216
+
217
+ initial_matches = [
218
+ _fuzzy_find_text(normalized_content, edit.old_text) for edit in normalized_edits
219
+ ]
220
+ base_content = (
221
+ normalize_for_fuzzy_match(normalized_content)
222
+ if any(match.used_fuzzy_match for match in initial_matches)
223
+ else normalized_content
224
+ )
225
+
226
+ matched_edits: list[_MatchedEdit] = []
227
+ for index, edit in enumerate(normalized_edits):
228
+ match = _fuzzy_find_text(base_content, edit.old_text)
229
+ if not match.found:
230
+ raise _not_found_error(path, index, len(normalized_edits))
231
+ occurrences = _count_occurrences(base_content, edit.old_text)
232
+ if occurrences > 1:
233
+ raise _duplicate_error(path, index, len(normalized_edits), occurrences)
234
+ matched_edits.append(
235
+ _MatchedEdit(index, match.index, match.length, edit.new_text)
236
+ )
237
+
238
+ matched_edits.sort(key=lambda edit: edit.match_index)
239
+ for previous, current in zip(matched_edits, matched_edits[1:], strict=False):
240
+ if previous.match_index + previous.match_length > current.match_index:
241
+ raise PiCompatEditError(
242
+ f"edits[{previous.edit_index}] and edits[{current.edit_index}] "
243
+ f"overlap in {path}. Merge them into one edit or target disjoint regions."
244
+ )
245
+
246
+ new_content = base_content
247
+ for matched_edit in reversed(matched_edits):
248
+ new_content = (
249
+ new_content[: matched_edit.match_index]
250
+ + matched_edit.new_text
251
+ + new_content[matched_edit.match_index + matched_edit.match_length :]
252
+ )
253
+ if base_content == new_content:
254
+ raise _no_change_error(path, len(normalized_edits))
255
+ return base_content, new_content
256
+
257
+
258
+ def _invalid_result(message: str) -> PiCompatOperationResult:
259
+ return PiCompatOperationResult(
260
+ model_text=sanitize_text(message),
261
+ truncated=False,
262
+ error_kind=PiCompatErrorKind.INVALID_ARGUMENTS,
263
+ exit_code=None,
264
+ changed_path=None,
265
+ side_effect_state=PiCompatSideEffectState.NOT_ATTEMPTED,
266
+ )
267
+
268
+
269
+ def _error_code(error: OSError | UnicodeError) -> str:
270
+ if isinstance(error, OSError) and error.errno == errno.ENOENT:
271
+ return "ENOENT"
272
+ if isinstance(error, OSError) and error.errno in {errno.EACCES, errno.EPERM}:
273
+ return "EACCES"
274
+ return type(error).__name__
275
+
276
+
277
+ def _filesystem_error_result(
278
+ error: OSError | UnicodeError,
279
+ *,
280
+ model_path: str,
281
+ write_started: bool,
282
+ ) -> PiCompatOperationResult:
283
+ if isinstance(error, FileNotFoundError):
284
+ kind = PiCompatErrorKind.NOT_FOUND
285
+ elif isinstance(error, PermissionError):
286
+ kind = PiCompatErrorKind.PERMISSION_DENIED
287
+ else:
288
+ kind = PiCompatErrorKind.IO_ERROR
289
+ return PiCompatOperationResult(
290
+ model_text=sanitize_text(
291
+ f"Could not edit file: {model_path}. Error code: {_error_code(error)}."
292
+ ),
293
+ truncated=False,
294
+ error_kind=kind,
295
+ exit_code=None,
296
+ changed_path=None,
297
+ side_effect_state=(
298
+ PiCompatSideEffectState.COMPLETION_UNKNOWN
299
+ if write_started
300
+ else PiCompatSideEffectState.NOT_ATTEMPTED
301
+ ),
302
+ )
303
+
304
+
305
+ def _coerce_edits(edits: Sequence[Mapping[str, object]]) -> list[Edit] | None:
306
+ coerced: list[Edit] = []
307
+ for edit in edits:
308
+ if not isinstance(edit, Mapping):
309
+ return None
310
+ old_text = edit.get("oldText")
311
+ new_text = edit.get("newText")
312
+ if not isinstance(old_text, str) or not isinstance(new_text, str):
313
+ return None
314
+ coerced.append(Edit(old_text, new_text))
315
+ return coerced
316
+
317
+
318
+ def execute_edit(
319
+ *,
320
+ cwd: Path,
321
+ path: str,
322
+ edits: Sequence[Mapping[str, object]],
323
+ ) -> PiCompatOperationResult:
324
+ """Apply all Pi edits atomically after validating them against one snapshot."""
325
+
326
+ if not _is_absolute_cwd(cwd):
327
+ return _invalid_result("cwd must be an absolute path")
328
+ if "\x00" in str(cwd):
329
+ return _invalid_result("cwd must not contain NUL bytes")
330
+ if not isinstance(path, str):
331
+ return _invalid_result("path must be a string")
332
+ if "\x00" in path:
333
+ return _invalid_result("path must not contain NUL bytes")
334
+ if isinstance(edits, (str, bytes)) or not isinstance(edits, Sequence):
335
+ return _invalid_result("edits must be an array")
336
+ typed_edits = _coerce_edits(edits)
337
+ if typed_edits is None:
338
+ return _invalid_result("each edit must contain string oldText and newText")
339
+ if not typed_edits:
340
+ return _invalid_result(
341
+ "Edit tool input is invalid. edits must contain at least one replacement."
342
+ )
343
+
344
+ write_started = False
345
+ try:
346
+ absolute_path = resolve_to_cwd(path, cwd)
347
+ with file_mutation_lock(absolute_path):
348
+ try:
349
+ raw_content = absolute_path.read_bytes().decode(
350
+ "utf-8", errors="replace"
351
+ )
352
+ bom, content = strip_bom(raw_content)
353
+ original_ending = detect_line_ending(content)
354
+ _, new_content = apply_edits_to_normalized_content(
355
+ normalize_to_lf(content), typed_edits, path
356
+ )
357
+ final_content = bom + restore_line_endings(new_content, original_ending)
358
+ write_started = True
359
+ with absolute_path.open("w", encoding="utf-8", newline="") as output:
360
+ output.write(final_content)
361
+ except PiCompatEditError as error:
362
+ return PiCompatOperationResult(
363
+ model_text=sanitize_text(str(error)),
364
+ truncated=False,
365
+ error_kind=PiCompatErrorKind.CONFLICT,
366
+ exit_code=None,
367
+ changed_path=None,
368
+ side_effect_state=PiCompatSideEffectState.NOT_ATTEMPTED,
369
+ )
370
+ except (OSError, UnicodeError) as error:
371
+ return _filesystem_error_result(
372
+ error,
373
+ model_path=path,
374
+ write_started=write_started,
375
+ )
376
+ except _PathValidationError as error:
377
+ return _invalid_result(str(error))
378
+ except UnicodeError:
379
+ return _invalid_result("path cannot be encoded by this filesystem")
380
+
381
+ return PiCompatOperationResult(
382
+ model_text=sanitize_text(
383
+ f"Successfully replaced {len(typed_edits)} block(s) in {path}."
384
+ ),
385
+ truncated=False,
386
+ error_kind=None,
387
+ exit_code=None,
388
+ changed_path=absolute_path,
389
+ side_effect_state=PiCompatSideEffectState.CONFIRMED_COMPLETE,
390
+ )
@@ -0,0 +1,57 @@
1
+ """In-process serialization for writes and edits targeting one filesystem path."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import threading
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Iterator
11
+
12
+ __all__ = ["file_mutation_lock"]
13
+
14
+ _registration_lock = threading.Lock()
15
+
16
+
17
+ @dataclass
18
+ class _PathLockEntry:
19
+ lock: threading.RLock
20
+ users: int = 0
21
+
22
+
23
+ _path_locks: dict[str, _PathLockEntry] = {}
24
+
25
+
26
+ def _mutation_key(path: Path) -> str:
27
+ absolute = Path(os.path.abspath(path))
28
+ try:
29
+ return str(absolute.resolve(strict=True))
30
+ except (OSError, RuntimeError):
31
+ return os.path.normcase(os.path.normpath(str(absolute)))
32
+
33
+
34
+ @contextmanager
35
+ def file_mutation_lock(path: Path) -> Iterator[None]:
36
+ """Serialize mutations to the same existing or lexical target path.
37
+
38
+ ``users`` includes both lock holders and queued callers, so the entry can
39
+ disappear only after the last caller has released it.
40
+ """
41
+
42
+ key = _mutation_key(path)
43
+ with _registration_lock:
44
+ entry = _path_locks.get(key)
45
+ if entry is None:
46
+ entry = _PathLockEntry(lock=threading.RLock())
47
+ _path_locks[key] = entry
48
+ entry.users += 1
49
+
50
+ try:
51
+ with entry.lock:
52
+ yield
53
+ finally:
54
+ with _registration_lock:
55
+ entry.users -= 1
56
+ if entry.users == 0 and _path_locks.get(key) is entry:
57
+ del _path_locks[key]