world-model-optimizer 0.2.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 (308) hide show
  1. llm_waterfall/LICENSE +21 -0
  2. llm_waterfall/__init__.py +53 -0
  3. llm_waterfall/adapters/__init__.py +36 -0
  4. llm_waterfall/adapters/anthropic.py +105 -0
  5. llm_waterfall/adapters/aws_mantle.py +47 -0
  6. llm_waterfall/adapters/azure_openai.py +71 -0
  7. llm_waterfall/adapters/base.py +51 -0
  8. llm_waterfall/adapters/bedrock.py +309 -0
  9. llm_waterfall/adapters/openai.py +130 -0
  10. llm_waterfall/classify.py +184 -0
  11. llm_waterfall/pricing.py +110 -0
  12. llm_waterfall/py.typed +0 -0
  13. llm_waterfall/types.py +295 -0
  14. llm_waterfall/waterfall.py +255 -0
  15. wmo/__init__.py +38 -0
  16. wmo/agents/__init__.py +7 -0
  17. wmo/agents/default.py +29 -0
  18. wmo/agents/meta.py +55 -0
  19. wmo/agents/optimizer.py +55 -0
  20. wmo/agents/project.py +928 -0
  21. wmo/cli/__init__.py +5 -0
  22. wmo/cli/agent_session.py +1123 -0
  23. wmo/cli/app.py +2489 -0
  24. wmo/cli/e2b_cmds.py +212 -0
  25. wmo/cli/eval_closed_loop.py +207 -0
  26. wmo/cli/harness_app.py +1147 -0
  27. wmo/cli/harness_distill.py +659 -0
  28. wmo/cli/hosted_session.py +880 -0
  29. wmo/cli/ingest_cmd.py +165 -0
  30. wmo/cli/model_roles.py +82 -0
  31. wmo/cli/platform_cmds.py +372 -0
  32. wmo/cli/route_app.py +274 -0
  33. wmo/cli/session_state.py +243 -0
  34. wmo/cli/ui.py +1107 -0
  35. wmo/cli/workspace_sync.py +504 -0
  36. wmo/config/__init__.py +60 -0
  37. wmo/config/card.py +129 -0
  38. wmo/config/config.py +367 -0
  39. wmo/config/dotenv.py +67 -0
  40. wmo/config/settings.py +128 -0
  41. wmo/config/store.py +177 -0
  42. wmo/conftest.py +19 -0
  43. wmo/connect/__init__.py +88 -0
  44. wmo/connect/apps.py +78 -0
  45. wmo/connect/brave.py +284 -0
  46. wmo/connect/connector.py +79 -0
  47. wmo/connect/credentials.py +164 -0
  48. wmo/connect/github.py +321 -0
  49. wmo/connect/google.py +627 -0
  50. wmo/connect/notion.py +790 -0
  51. wmo/connect/oauth.py +461 -0
  52. wmo/connect/slack.py +555 -0
  53. wmo/connect/store.py +199 -0
  54. wmo/connect/types.py +156 -0
  55. wmo/core/__init__.py +21 -0
  56. wmo/core/parsing.py +281 -0
  57. wmo/core/render.py +271 -0
  58. wmo/core/text.py +40 -0
  59. wmo/core/types.py +116 -0
  60. wmo/distill/__init__.py +14 -0
  61. wmo/distill/agents.py +140 -0
  62. wmo/distill/config.py +1006 -0
  63. wmo/distill/cost.py +437 -0
  64. wmo/distill/data.py +921 -0
  65. wmo/distill/deadlines.py +254 -0
  66. wmo/distill/fake_tinker.py +734 -0
  67. wmo/distill/gate.py +122 -0
  68. wmo/distill/loop.py +3499 -0
  69. wmo/distill/renderers.py +399 -0
  70. wmo/distill/rendering.py +620 -0
  71. wmo/distill/rollouts.py +726 -0
  72. wmo/distill/samples.py +195 -0
  73. wmo/distill/store.py +829 -0
  74. wmo/distill/teacher.py +714 -0
  75. wmo/distill/tokens.py +535 -0
  76. wmo/distill/tracking.py +552 -0
  77. wmo/distill/tripwire.py +411 -0
  78. wmo/distill/xtoken/byte_offsets.py +152 -0
  79. wmo/distill/xtoken/chunks.py +457 -0
  80. wmo/distill/xtoken/prompt_logprobs.py +475 -0
  81. wmo/distill/xtoken/teacher_render.py +346 -0
  82. wmo/engine/__init__.py +28 -0
  83. wmo/engine/autoconfig.py +367 -0
  84. wmo/engine/build.py +346 -0
  85. wmo/engine/demo.py +77 -0
  86. wmo/engine/eval_suites.py +245 -0
  87. wmo/engine/grounding.py +491 -0
  88. wmo/engine/knowledge.py +291 -0
  89. wmo/engine/loader.py +36 -0
  90. wmo/engine/play.py +92 -0
  91. wmo/engine/prompts.py +99 -0
  92. wmo/engine/replay.py +443 -0
  93. wmo/engine/reporting.py +58 -0
  94. wmo/engine/workspace.py +468 -0
  95. wmo/engine/world_model.py +568 -0
  96. wmo/env/__init__.py +22 -0
  97. wmo/env/base.py +121 -0
  98. wmo/env/closed_loop.py +229 -0
  99. wmo/env/episode.py +107 -0
  100. wmo/env/llm_agent.py +93 -0
  101. wmo/env/scenarios.py +73 -0
  102. wmo/evals/__init__.py +52 -0
  103. wmo/evals/agreement.py +110 -0
  104. wmo/evals/base.py +45 -0
  105. wmo/evals/closed_loop.py +480 -0
  106. wmo/evals/failover.py +96 -0
  107. wmo/evals/gold.py +127 -0
  108. wmo/evals/grid.py +394 -0
  109. wmo/evals/grid_plot.py +205 -0
  110. wmo/evals/harbor/__init__.py +27 -0
  111. wmo/evals/harbor/agent.py +573 -0
  112. wmo/evals/harbor/ctrf.py +171 -0
  113. wmo/evals/harbor/e2b_environment.py +587 -0
  114. wmo/evals/harbor/e2b_template_policy.py +144 -0
  115. wmo/evals/harbor/scorer.py +875 -0
  116. wmo/evals/harbor/tasks.py +140 -0
  117. wmo/evals/open_loop.py +194 -0
  118. wmo/evals/tasks.py +53 -0
  119. wmo/harness/__init__.py +51 -0
  120. wmo/harness/code_runtime.py +288 -0
  121. wmo/harness/create.py +1191 -0
  122. wmo/harness/delta.py +220 -0
  123. wmo/harness/doc.py +556 -0
  124. wmo/harness/e2b_ledger.py +342 -0
  125. wmo/harness/e2b_reap.py +476 -0
  126. wmo/harness/e2b_sandbox.py +350 -0
  127. wmo/harness/environment.py +35 -0
  128. wmo/harness/live_session.py +543 -0
  129. wmo/harness/mutate.py +343 -0
  130. wmo/harness/pi_e2b.py +1710 -0
  131. wmo/harness/pi_entry/entry.ts +268 -0
  132. wmo/harness/pi_entry/runner_frames.ts +92 -0
  133. wmo/harness/pi_entry/runner_live.ts +587 -0
  134. wmo/harness/pi_entry/runner_service.ts +270 -0
  135. wmo/harness/pi_entry/runner_stdio.ts +374 -0
  136. wmo/harness/pi_entry/runner_termination.ts +142 -0
  137. wmo/harness/pi_local.py +262 -0
  138. wmo/harness/pi_runtime.py +495 -0
  139. wmo/harness/pi_vendor.py +65 -0
  140. wmo/harness/population.py +509 -0
  141. wmo/harness/project_proposer.py +569 -0
  142. wmo/harness/proposer.py +977 -0
  143. wmo/harness/runner_link.py +619 -0
  144. wmo/harness/runtime.py +389 -0
  145. wmo/harness/scoring.py +247 -0
  146. wmo/harness/skills.py +116 -0
  147. wmo/harness/source_tree.py +319 -0
  148. wmo/harness/store.py +176 -0
  149. wmo/harness/tools.py +105 -0
  150. wmo/harness/vendor/manifest.sha256 +58 -0
  151. wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
  152. wmo/harness/vendor/pi-agent/LICENSE +21 -0
  153. wmo/harness/vendor/pi-agent/README.md +488 -0
  154. wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
  155. wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
  156. wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
  157. wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
  158. wmo/harness/vendor/pi-agent/docs/models.md +966 -0
  159. wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
  160. wmo/harness/vendor/pi-agent/package.json +60 -0
  161. wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
  162. wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
  163. wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
  164. wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
  165. wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
  166. wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
  167. wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
  168. wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
  169. wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
  170. wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
  171. wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
  172. wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
  173. wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
  174. wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
  175. wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
  176. wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
  177. wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
  178. wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
  179. wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
  180. wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
  181. wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
  182. wmo/harness/vendor/pi-agent/src/index.ts +44 -0
  183. wmo/harness/vendor/pi-agent/src/node.ts +2 -0
  184. wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
  185. wmo/harness/vendor/pi-agent/src/types.ts +428 -0
  186. wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
  187. wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
  188. wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
  189. wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
  190. wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
  191. wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
  192. wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
  193. wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
  194. wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
  195. wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
  196. wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
  197. wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
  198. wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
  199. wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
  200. wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
  201. wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
  202. wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
  203. wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
  204. wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
  205. wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
  206. wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
  207. wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
  208. wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
  209. wmo/harness/vendor/vendor_pi.sh +59 -0
  210. wmo/harness/workspace_patch.py +270 -0
  211. wmo/ingest/__init__.py +47 -0
  212. wmo/ingest/adapter.py +72 -0
  213. wmo/ingest/base.py +114 -0
  214. wmo/ingest/braintrust.py +339 -0
  215. wmo/ingest/detect.py +126 -0
  216. wmo/ingest/langfuse.py +291 -0
  217. wmo/ingest/langsmith.py +444 -0
  218. wmo/ingest/mastra.py +330 -0
  219. wmo/ingest/messages.py +170 -0
  220. wmo/ingest/normalize.py +679 -0
  221. wmo/ingest/otel_genai.py +69 -0
  222. wmo/ingest/otel_writer.py +100 -0
  223. wmo/ingest/phoenix.py +150 -0
  224. wmo/ingest/postgres.py +246 -0
  225. wmo/ingest/posthog.py +320 -0
  226. wmo/ingest/quality.py +28 -0
  227. wmo/ingest/stream.py +209 -0
  228. wmo/ingest/testdata/sample_otlp.json +60 -0
  229. wmo/ingest/testdata/sample_spans.jsonl +3 -0
  230. wmo/optimize/__init__.py +25 -0
  231. wmo/optimize/base.py +143 -0
  232. wmo/optimize/gepa.py +806 -0
  233. wmo/optimize/judge.py +262 -0
  234. wmo/optimize/judge_quality.py +359 -0
  235. wmo/optimize/knn.py +468 -0
  236. wmo/optimize/numeric.py +152 -0
  237. wmo/optimize/outcomes.py +103 -0
  238. wmo/optimize/policy.py +669 -0
  239. wmo/optimize/report.py +231 -0
  240. wmo/optimize/reward.py +129 -0
  241. wmo/optimize/routing.py +373 -0
  242. wmo/platform/__init__.py +6 -0
  243. wmo/platform/auth.py +115 -0
  244. wmo/platform/client.py +551 -0
  245. wmo/platform/credentials.py +126 -0
  246. wmo/platform/transfer.py +158 -0
  247. wmo/providers/__init__.py +40 -0
  248. wmo/providers/_bedrock_chat.py +155 -0
  249. wmo/providers/_openai_common.py +182 -0
  250. wmo/providers/_responses_common.py +472 -0
  251. wmo/providers/anthropic.py +134 -0
  252. wmo/providers/azure_openai.py +296 -0
  253. wmo/providers/base.py +300 -0
  254. wmo/providers/bedrock.py +312 -0
  255. wmo/providers/models.py +205 -0
  256. wmo/providers/openai.py +143 -0
  257. wmo/providers/openai_responses.py +240 -0
  258. wmo/providers/pool.py +170 -0
  259. wmo/providers/registry.py +73 -0
  260. wmo/providers/retry.py +151 -0
  261. wmo/providers/tinker.py +936 -0
  262. wmo/providers/waterfall.py +336 -0
  263. wmo/research/__init__.py +81 -0
  264. wmo/research/ablation.py +133 -0
  265. wmo/research/concurrency_plot.py +523 -0
  266. wmo/research/concurrency_run.py +240 -0
  267. wmo/research/concurrency_scaling.py +270 -0
  268. wmo/research/gepa_scaling.py +274 -0
  269. wmo/research/pipeline.py +198 -0
  270. wmo/research/scaling_split.py +82 -0
  271. wmo/research/scenario_fidelity.py +198 -0
  272. wmo/research/scenario_recovery.py +92 -0
  273. wmo/research/seed_stability.py +90 -0
  274. wmo/research/trace_scaling.py +348 -0
  275. wmo/retrieval/__init__.py +6 -0
  276. wmo/retrieval/embedders.py +105 -0
  277. wmo/retrieval/leakfree.py +52 -0
  278. wmo/retrieval/retriever.py +173 -0
  279. wmo/scenarios/__init__.py +58 -0
  280. wmo/scenarios/builder.py +152 -0
  281. wmo/scenarios/mining/__init__.py +27 -0
  282. wmo/scenarios/mining/clustering.py +171 -0
  283. wmo/scenarios/mining/facets.py +226 -0
  284. wmo/scenarios/mining/selection.py +220 -0
  285. wmo/scenarios/synthesis/__init__.py +6 -0
  286. wmo/scenarios/synthesis/scenario_set.py +63 -0
  287. wmo/scenarios/synthesis/synthesizer.py +85 -0
  288. wmo/scenarios/verification/__init__.py +17 -0
  289. wmo/scenarios/verification/judge.py +97 -0
  290. wmo/scenarios/verification/verify.py +135 -0
  291. wmo/serving/__init__.py +5 -0
  292. wmo/serving/builds.py +451 -0
  293. wmo/serving/chat.py +878 -0
  294. wmo/serving/endpoint_config.py +64 -0
  295. wmo/serving/savings.py +250 -0
  296. wmo/serving/server.py +553 -0
  297. wmo/serving/traces_source.py +206 -0
  298. wmo/telemetry.py +213 -0
  299. wmo/tracking/__init__.py +36 -0
  300. wmo/tracking/clock.py +24 -0
  301. wmo/tracking/metered.py +125 -0
  302. wmo/tracking/pricing.py +99 -0
  303. wmo/tracking/store.py +31 -0
  304. wmo/tracking/tracker.py +149 -0
  305. world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
  306. world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
  307. world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
  308. world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
wmo/harness/delta.py ADDED
@@ -0,0 +1,220 @@
1
+ """`HarnessDelta`: the typed update object `wmo optimize` searches through.
2
+
3
+ The update representation IS the search space: everything the meta-agent can learn about *how to
4
+ improve harnesses* is bounded by what the update object can express. A raw file edit expresses
5
+ almost nothing — no typed target, no assertion about what it believed it was editing, no per-change
6
+ rationale, no verdict. A delta expresses all four:
7
+
8
+ - **trigger** (`FailureSignature`): the clustered failure mechanism this delta answers to, so the
9
+ archive can ask "which kinds of edits work on which failure classes?" instead of "what changed?".
10
+ - **preconditions**: `surface_id -> expected content hash` of every surface the proposer read
11
+ before editing. Application is atomic — ANY mismatch rejects the whole delta before a token of
12
+ eval budget is spent, so a proposal drafted against one parent can never silently misapply to
13
+ another.
14
+ - **ops** (`SurfaceOp`): add/replace/remove keyed by surface *identity*, each carrying its own
15
+ rationale bound to the op it justifies.
16
+ - **verdict** (`GateRecord`): the acceptance decision and the gate deltas that produced it, written
17
+ back onto the delta. The archive is a lineage of audited deltas, not a pile of snapshots.
18
+
19
+ `expected_effect` makes every delta a falsifiable prediction: at gate time the trigger cluster is
20
+ re-checked and the outcome lands in `verdict.reason`, measuring the proposer's calibration over
21
+ time, not just its win rate.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import hashlib
27
+ from typing import Literal
28
+
29
+ from pydantic import BaseModel, Field, model_validator
30
+
31
+ from wmo.core.text import validate_durable_text
32
+ from wmo.harness.doc import HarnessDoc, Surface, SurfaceKind
33
+
34
+
35
+ class FailureSignature(BaseModel):
36
+ """The clustered failure mechanism a delta answers to: WHY it exists, queryably.
37
+
38
+ Built deterministically from a closed-loop report (`wmo.harness.create.cluster_failures`),
39
+ never free-typed by the proposer — so the archive's mechanism labels are comparable across
40
+ deltas and runs.
41
+ """
42
+
43
+ mechanism: str # e.g. the shared unmet assertion, or "none: all tasks pass"
44
+ task_ids: list[str] = Field(default_factory=list) # the failing tasks exhibiting it
45
+ unmet_assertions: list[str] = Field(default_factory=list) # deduped, order-stable
46
+
47
+ @model_validator(mode="after")
48
+ def _validate_text(self) -> FailureSignature:
49
+ validate_durable_text(self.mechanism, field="failure mechanism")
50
+ for task_id in self.task_ids:
51
+ validate_durable_text(task_id, field="failure task id")
52
+ for assertion in self.unmet_assertions:
53
+ validate_durable_text(assertion, field="failure assertion")
54
+ return self
55
+
56
+
57
+ class SurfaceOp(BaseModel):
58
+ """One typed edit to one surface, addressed by identity, justified in place."""
59
+
60
+ op: Literal["add", "replace", "remove"]
61
+ surface_id: str
62
+ kind: SurfaceKind | None = None # required on add; on replace must match the existing kind
63
+ content: str | None = None # the full new content (component rewrite, never a line diff)
64
+ budget: int | None = Field(default=None, ge=1) # replace: None inherits the existing budget
65
+ # File path for multi-file CODE surfaces (e.g. vendored pi `code:src-...`): on replace, None
66
+ # inherits the existing surface's path (so editing pi source keeps it a valid pathful surface);
67
+ # required to introduce a brand-new pathful code surface.
68
+ path: str | None = None
69
+ rationale: str # why THIS op, bound to the op — not one motivation string for the whole delta
70
+
71
+ @model_validator(mode="after")
72
+ def _validate_shape(self) -> SurfaceOp:
73
+ if self.content is not None:
74
+ validate_durable_text(self.content, field=f"{self.surface_id!r} operation content")
75
+ validate_durable_text(self.rationale, field=f"{self.surface_id!r} operation rationale")
76
+ if self.op == "add" and self.kind is None:
77
+ raise ValueError(f"add of {self.surface_id!r} must declare a kind")
78
+ if self.op in ("add", "replace") and self.content is None:
79
+ raise ValueError(f"{self.op} of {self.surface_id!r} must carry content")
80
+ if self.op == "remove" and self.content is not None:
81
+ raise ValueError(
82
+ f"remove of {self.surface_id!r} carries content; a remove deletes, it never writes"
83
+ )
84
+ if not self.rationale.strip():
85
+ raise ValueError(f"{self.op} of {self.surface_id!r} has no rationale")
86
+ return self
87
+
88
+
89
+ class GateRecord(BaseModel):
90
+ """The acceptance verdict, filled at evaluation time and persisted on the delta."""
91
+
92
+ suite_delta: float = 0.0 # regression suite: child - champion (tier 1; >= 0 to pass)
93
+ suite_fraction_delta: float = 0.0 # assertion credit; vetoes only when suite success ties
94
+ full_delta: float = 0.0 # full split: child - best seen (tier 2; >= 0 to pass)
95
+ full_fraction_delta: float = 0.0 # assertion credit; vetoes only when full success ties
96
+ holdout_delta: float | None = None # held-out split (tier 3; None when no holdout given)
97
+ holdout_fraction_delta: float | None = None # dense held-out signal when success ties
98
+ accepted: bool
99
+ reason: str # accept/reject reasoning, incl. whether `expected_effect` came true
100
+
101
+ @model_validator(mode="after")
102
+ def _validate_text(self) -> GateRecord:
103
+ validate_durable_text(self.reason, field="gate reason")
104
+ return self
105
+
106
+
107
+ class HarnessDelta(BaseModel):
108
+ """One proposed update to a `HarnessDoc`, with its audit trail.
109
+
110
+ Lineage is by content, not name: `parent_doc_hash` names exactly the document the delta was
111
+ proposed against, and `child_doc_hash` (recorded on successful application) names exactly what
112
+ it produced. A doc is reconstructable by folding accepted deltas from any ancestor snapshot.
113
+ """
114
+
115
+ delta_id: str
116
+ parent_doc_hash: str
117
+ trigger: FailureSignature
118
+ # surface_id -> expected content hash of the parent surface the proposer read. Every
119
+ # replace/remove target MUST appear here; application atomically rejects on any mismatch.
120
+ preconditions: dict[str, str] = Field(default_factory=dict)
121
+ ops: list[SurfaceOp] = Field(min_length=1)
122
+ expected_effect: str # falsifiable: e.g. "the trigger cluster's tasks flip to pass"
123
+ child_doc_hash: str | None = None # set by apply_delta
124
+ verdict: GateRecord | None = None # set by the gate; None until evaluated
125
+
126
+ @model_validator(mode="after")
127
+ def _validate_text(self) -> HarnessDelta:
128
+ validate_durable_text(self.expected_effect, field="delta expected effect")
129
+ return self
130
+
131
+
132
+ def compute_delta_id(parent_doc_hash: str, ops: list[SurfaceOp]) -> str:
133
+ """Deterministic delta identity: what it does to what, independent of when it was proposed."""
134
+ joined = parent_doc_hash + "".join(
135
+ f"\x00{op.op}\x00{op.surface_id}\x00{op.content or ''}"
136
+ f"\x00{op.budget or ''}\x00{op.path or ''}"
137
+ for op in ops
138
+ )
139
+ return hashlib.blake2b(joined.encode("utf-8"), digest_size=16).hexdigest()
140
+
141
+
142
+ def apply_delta(parent: HarnessDoc, delta: HarnessDelta, child_name: str) -> HarnessDoc:
143
+ """Apply `delta` to `parent` atomically; returns the child with `delta.child_doc_hash` set.
144
+
145
+ Every check runs before anything is built, and the child itself re-validates as a whole
146
+ `HarnessDoc` at construction — an invalid delta must fail here, before any eval budget is
147
+ spent on the variant it would have produced. Raises `ValueError` on any violation.
148
+ """
149
+ _check_lineage(parent, delta)
150
+ _check_preconditions(parent, delta)
151
+ _check_ops(parent, delta)
152
+
153
+ surfaces = {s.id: s for s in parent.surfaces}
154
+ for op in delta.ops:
155
+ if op.op == "remove":
156
+ del surfaces[op.surface_id]
157
+ continue
158
+ existing = surfaces.get(op.surface_id)
159
+ # Narrowing only: SurfaceOp's model validator guarantees add carries a kind and
160
+ # add/replace carry content, and _check_ops guarantees replace targets exist.
161
+ kind = op.kind if op.kind is not None else existing.kind if existing else None
162
+ budget = op.budget if op.budget is not None else existing.budget if existing else None
163
+ # Carry the file path: a replace inherits the existing surface's path unless overridden,
164
+ # so mutating a vendored pi `code:src-...` surface stays a valid pathful CODE surface.
165
+ path = op.path if op.path is not None else existing.path if existing else None
166
+ if kind is None or op.content is None:
167
+ raise ValueError(f"malformed {op.op} of {op.surface_id!r}")
168
+ surfaces[op.surface_id] = Surface(
169
+ id=op.surface_id, kind=kind, content=op.content, budget=budget, path=path
170
+ )
171
+
172
+ child = HarnessDoc(name=child_name, surfaces=list(surfaces.values()))
173
+ delta.child_doc_hash = child.doc_hash
174
+ return child
175
+
176
+
177
+ def _check_lineage(parent: HarnessDoc, delta: HarnessDelta) -> None:
178
+ if delta.parent_doc_hash != parent.doc_hash:
179
+ raise ValueError(
180
+ f"delta {delta.delta_id} was proposed against doc {delta.parent_doc_hash[:12]}, "
181
+ f"not {parent.doc_hash[:12]} ({parent.name} v{parent.version})"
182
+ )
183
+
184
+
185
+ def _check_preconditions(parent: HarnessDoc, delta: HarnessDelta) -> None:
186
+ for surface_id, expected in delta.preconditions.items():
187
+ surface = parent.surface(surface_id)
188
+ if surface is None:
189
+ raise ValueError(f"precondition on unknown surface {surface_id!r}")
190
+ if surface.content_hash != expected:
191
+ raise ValueError(
192
+ f"precondition mismatch on {surface_id!r}: expected {expected[:12]}, "
193
+ f"parent has {surface.content_hash[:12]} — the delta was drafted against "
194
+ "different content"
195
+ )
196
+
197
+
198
+ def _check_ops(parent: HarnessDoc, delta: HarnessDelta) -> None:
199
+ targets = [op.surface_id for op in delta.ops]
200
+ duplicates = sorted({t for t in targets if targets.count(t) > 1})
201
+ if duplicates:
202
+ raise ValueError(f"multiple ops target the same surface(s): {duplicates}")
203
+ for op in delta.ops:
204
+ existing = parent.surface(op.surface_id)
205
+ if op.op == "add":
206
+ if existing is not None:
207
+ raise ValueError(f"add of {op.surface_id!r}: the surface already exists")
208
+ else:
209
+ if existing is None:
210
+ raise ValueError(f"{op.op} of unknown surface {op.surface_id!r}")
211
+ if op.kind is not None and op.kind is not existing.kind:
212
+ raise ValueError(
213
+ f"{op.op} of {op.surface_id!r} declares kind {op.kind.value!r}; the surface "
214
+ f"is {existing.kind.value!r}"
215
+ )
216
+ if op.surface_id not in delta.preconditions:
217
+ raise ValueError(
218
+ f"{op.op} of {op.surface_id!r} has no precondition: an update must assert "
219
+ "the content hash of every surface it replaces or removes"
220
+ )