annoscript 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 (128) hide show
  1. annoscript/__init__.py +1 -0
  2. annoscript/_backend_profiles.py +17 -0
  3. annoscript/_client_common.py +287 -0
  4. annoscript/_data.py +37 -0
  5. annoscript/_dispatch_pool.py +47 -0
  6. annoscript/_env.py +19 -0
  7. annoscript/_examples.py +34 -0
  8. annoscript/_expressions.py +93 -0
  9. annoscript/_plan_viewer.html +953 -0
  10. annoscript/_remote_jobs.py +211 -0
  11. annoscript/_request_adapters.py +71 -0
  12. annoscript/_speculation.py +321 -0
  13. annoscript/_test_artifacts.py +33 -0
  14. annoscript/_test_evaluation_http.py +117 -0
  15. annoscript/_test_ir_runtime.py +138 -0
  16. annoscript/_test_job_process.py +166 -0
  17. annoscript/_test_plan_runtime.py +331 -0
  18. annoscript/_test_remote_client.py +46 -0
  19. annoscript/_test_remote_server.py +75 -0
  20. annoscript/_test_synthetic.py +67 -0
  21. annoscript/anno.py +270 -0
  22. annoscript/backends/__init__.py +13 -0
  23. annoscript/backends/_batch_wire.py +26 -0
  24. annoscript/backends/_bounded_grammar.py +97 -0
  25. annoscript/backends/_example_layout.py +23 -0
  26. annoscript/backends/_generation_policy.py +116 -0
  27. annoscript/backends/_grammar.py +83 -0
  28. annoscript/backends/_preview.py +13 -0
  29. annoscript/backends/_prompt.py +115 -0
  30. annoscript/backends/_shared_rules.py +38 -0
  31. annoscript/backends/adapters/__init__.py +17 -0
  32. annoscript/backends/adapters/chat.py +39 -0
  33. annoscript/backends/adapters/decision.py +119 -0
  34. annoscript/backends/base.py +427 -0
  35. annoscript/backends/config.py +67 -0
  36. annoscript/backends/decision.py +150 -0
  37. annoscript/backends/llm.py +338 -0
  38. annoscript/client.py +247 -0
  39. annoscript/client_tests.py +17 -0
  40. annoscript/compiler.py +708 -0
  41. annoscript/cross_version_tests.py +68 -0
  42. annoscript/fixtures/request-adapters-v1.json +4741 -0
  43. annoscript/formatter.py +171 -0
  44. annoscript/grammar_tests.py +176 -0
  45. annoscript/inspect.py +77 -0
  46. annoscript/ir.py +458 -0
  47. annoscript/job_cross_version_tests.py +118 -0
  48. annoscript/job_protocol.py +180 -0
  49. annoscript/jobs.py +255 -0
  50. annoscript/metrics.py +153 -0
  51. annoscript/p0_grammar_tests.py +147 -0
  52. annoscript/passes/__init__.py +40 -0
  53. annoscript/passes/_analysis.py +178 -0
  54. annoscript/passes/control_flow.py +68 -0
  55. annoscript/passes/dead_assignments.py +44 -0
  56. annoscript/passes/folding.py +104 -0
  57. annoscript/passes/propagation.py +53 -0
  58. annoscript/passes/temporaries.py +45 -0
  59. annoscript/plan.py +542 -0
  60. annoscript/plan_formatter.py +452 -0
  61. annoscript/plan_lowering.py +571 -0
  62. annoscript/preview.py +115 -0
  63. annoscript/remote.py +304 -0
  64. annoscript/runtime.py +861 -0
  65. annoscript/server/__init__.py +1 -0
  66. annoscript/server/__main__.py +49 -0
  67. annoscript/server/_plan_cache.py +79 -0
  68. annoscript/server/api.py +146 -0
  69. annoscript/server/metrics.py +140 -0
  70. annoscript/server/migrate.py +61 -0
  71. annoscript/server/releases.py +143 -0
  72. annoscript/server/store.py +429 -0
  73. annoscript/server/worker.py +290 -0
  74. annoscript/substring_tests.py +79 -0
  75. annoscript/test_advisory_lengths.py +115 -0
  76. annoscript/test_backends.py +181 -0
  77. annoscript/test_client.py +349 -0
  78. annoscript/test_compact_batch.py +95 -0
  79. annoscript/test_compiler.py +444 -0
  80. annoscript/test_decision_adapters.py +410 -0
  81. annoscript/test_decision_wire.py +190 -0
  82. annoscript/test_diagnostics.py +97 -0
  83. annoscript/test_engineering_p0.py +191 -0
  84. annoscript/test_environment.py +65 -0
  85. annoscript/test_example_backends.py +251 -0
  86. annoscript/test_examples.py +149 -0
  87. annoscript/test_field_names.py +221 -0
  88. annoscript/test_formatter.py +153 -0
  89. annoscript/test_fusion_scheduling.py +109 -0
  90. annoscript/test_generation_policy.py +180 -0
  91. annoscript/test_ir.py +148 -0
  92. annoscript/test_jobs.py +371 -0
  93. annoscript/test_jobs_server.py +511 -0
  94. annoscript/test_llm_batch.py +428 -0
  95. annoscript/test_metrics.py +59 -0
  96. annoscript/test_metrics_server.py +261 -0
  97. annoscript/test_minimal_fusion.py +59 -0
  98. annoscript/test_nullable_generate.py +169 -0
  99. annoscript/test_passes.py +408 -0
  100. annoscript/test_plan.py +522 -0
  101. annoscript/test_plan_constraints.py +305 -0
  102. annoscript/test_plan_formatter.py +272 -0
  103. annoscript/test_prompt_layout.py +140 -0
  104. annoscript/test_qwen_backend.py +181 -0
  105. annoscript/test_qwen_pilot_regression.py +180 -0
  106. annoscript/test_release_client.py +20 -0
  107. annoscript/test_remote.py +308 -0
  108. annoscript/test_request_adapters.py +295 -0
  109. annoscript/test_requirements.py +228 -0
  110. annoscript/test_rule_dedup.py +67 -0
  111. annoscript/test_runtime.py +314 -0
  112. annoscript/test_runtime_client.py +133 -0
  113. annoscript/test_source_fields.py +111 -0
  114. annoscript/test_speculation.py +458 -0
  115. annoscript/test_substring_backend.py +81 -0
  116. annoscript/test_synthetic_example.py +61 -0
  117. annoscript-0.1.0.dist-info/METADATA +145 -0
  118. annoscript-0.1.0.dist-info/RECORD +128 -0
  119. annoscript-0.1.0.dist-info/WHEEL +4 -0
  120. annoscript-0.1.0.dist-info/licenses/LICENSE +21 -0
  121. annoscript_examples/__init__.py +1 -0
  122. annoscript_examples/fixtures.py +3 -0
  123. annoscript_examples/request.execution_plan.json +3017 -0
  124. annoscript_examples/request.json +858 -0
  125. annoscript_examples/request.py +37 -0
  126. annoscript_examples/request_preview.decision.json +23 -0
  127. annoscript_examples/request_preview.generation.json +20 -0
  128. annoscript_examples/run.py +24 -0
@@ -0,0 +1,305 @@
1
+ """Eager check speculation without weakening bindings, paths or memory order."""
2
+
3
+ import asyncio
4
+ from dataclasses import replace
5
+ import unittest
6
+
7
+ from annoscript import plan as p
8
+ from annoscript.compiler import compile_program
9
+ from annoscript.anno import GenerateResponse, constrain
10
+ from annoscript.plan_lowering import lower_to_plan
11
+ from annoscript._test_ir_runtime import evaluate as evaluate_ir
12
+ from annoscript._test_plan_runtime import _Executor, evaluate
13
+ from annoscript.test_plan import PureAnnoScript, checked_loop
14
+
15
+
16
+ def checked_chain(anno):
17
+ text = anno.generate("check", max_length=512).text
18
+ constrain(text == "ok", message="check failed")
19
+ other = anno.generate("independent", max_length=512).text
20
+ return anno.generate(other, max_length=512).text
21
+
22
+
23
+ def checked_constant_return(anno):
24
+ text = anno.generate("check", max_length=512).text
25
+ constrain(text == "ok", message="check failed")
26
+ return "done"
27
+
28
+
29
+ def checked_nested_return(anno):
30
+ if anno.state:
31
+ if anno.state[0]:
32
+ text = anno.generate("check", max_length=512).text
33
+ constrain(text == "ok", message="check failed")
34
+ return "done"
35
+
36
+
37
+ def checked_unused_loop(anno):
38
+ for item in anno.state:
39
+ text = anno.generate(item, max_length=512).text
40
+ constrain(text == "ok", message="check failed")
41
+ return "done"
42
+
43
+
44
+ def checked_data(anno):
45
+ text = anno.generate("first", max_length=512).text
46
+ constrain(text != "")
47
+ return anno.generate(text + " second", max_length=512).text
48
+
49
+
50
+ def checked_control(anno):
51
+ if anno.generate("predicate", max_length=512).text == "selected":
52
+ text = anno.generate("selected", max_length=512).text
53
+ constrain(text == "ok", message="selected failed")
54
+ return anno.generate("independent", max_length=512).text
55
+
56
+
57
+ def checked_memory(anno):
58
+ data = {"value": "initial"}
59
+ alias = data
60
+ alias["value"] = anno.generate("updated", max_length=512).text
61
+ constrain(data["value"] == "updated", message=data["value"])
62
+ alias["value"] = "later"
63
+ return data["value"]
64
+
65
+
66
+ def checked_container_truth(anno):
67
+ data = {}
68
+ constrain(data, message="empty container")
69
+ data["key"] = "later"
70
+ return data
71
+
72
+
73
+ def checked_message(anno):
74
+ data = {"message": "original diagnostic"}
75
+ constrain(anno.state, message=data["message"])
76
+ data["message"] = "changed diagnostic"
77
+ return data
78
+
79
+
80
+ def checked_rebinding(anno):
81
+ value = anno.generate("original", max_length=512).text
82
+ constrain(value == "original")
83
+ value = "rebound"
84
+ return value
85
+
86
+
87
+ def checked_shared(anno):
88
+ a = anno.generate("same", max_length=512).text
89
+ constrain(a == anno.state, message="shared check failed")
90
+ b = anno.generate("same", max_length=512).text
91
+ return [a, b]
92
+
93
+
94
+ def checked_memory_loop(anno):
95
+ data = {"value": "initial"}
96
+ for item in anno.state:
97
+ data["value"] = item
98
+ text = anno.generate(item, max_length=512).text
99
+ constrain(data["value"] == text, message="wrong iteration")
100
+ return data["value"]
101
+
102
+
103
+ class ConstraintPlanTests(unittest.TestCase):
104
+ def plan(self, function):
105
+ return lower_to_plan(compile_program(function))
106
+
107
+ def test_only_explicit_ordering_edges_are_relaxed(self):
108
+ plan = self.plan(checked_memory)
109
+ eager, strict = plan.dependencies_for(), plan.dependencies_for("strict")
110
+ self.assertEqual(strict, plan.dependencies)
111
+ self.assertTrue(any(d.kind == "constraint" and d.strict_only for d in strict))
112
+ for d in strict:
113
+ if d.kind in ("data", "control", "memory"):
114
+ self.assertIn(d, eager)
115
+ self.assertFalse(d.strict_only)
116
+ self.assertFalse(any(d.kind in ("constraint", "failure_order") for d in eager))
117
+ self.assertEqual(p.ExecutionPlan.from_json(plan.to_json()), plan)
118
+ # Old plans remain conservative. The constraint *kind* is not itself
119
+ # permission to remove an edge, and missing flags default to False.
120
+ legacy = plan.to_dict()
121
+ for d in legacy["dependencies"]:
122
+ d.pop("strict_only")
123
+ restored = p.ExecutionPlan.from_dict(legacy)
124
+ self.assertTrue(any(d.kind == "constraint" for d in restored.dependencies_for()))
125
+ for kind in ("data", "control", "memory"):
126
+ original = self.plan(checked_control if kind == "control" else checked_memory)
127
+ edge = next(d for d in original.dependencies if d.kind == kind)
128
+ broken = replace(original, dependencies=tuple(replace(d, strict_only=True) if d == edge else d for d in original.dependencies))
129
+ with self.assertRaisesRegex(p.PlanDecodeError, "cannot be strict-only"):
130
+ p.ExecutionPlan.from_json(broken.to_json())
131
+
132
+ def test_eager_constraint_failure_cancels_inflight_and_blocks_new_calls(self):
133
+ class Concurrent(PureAnnoScript):
134
+ async def generate(self, instructions, **kwargs):
135
+ self.calls.append((instructions, kwargs))
136
+ if instructions == "check":
137
+ await self.independent_started.wait()
138
+ return GenerateResponse("bad")
139
+ if instructions == "independent":
140
+ self.independent_started.set()
141
+ try:
142
+ await asyncio.Event().wait()
143
+ except asyncio.CancelledError:
144
+ self.cancelled = True
145
+ raise
146
+ raise AssertionError("dispatched after failure")
147
+
148
+ async def exercise():
149
+ anno = Concurrent()
150
+ anno.independent_started, anno.cancelled = asyncio.Event(), False
151
+ executor = _Executor(self.plan(checked_chain).to_dict(), anno, "eager", "reverse")
152
+ with self.assertRaisesRegex(ValueError, "check failed"):
153
+ await executor.run()
154
+ self.assertCountEqual([x[0] for x in anno.calls], ["check", "independent"])
155
+ self.assertTrue(anno.cancelled)
156
+
157
+ asyncio.run(asyncio.wait_for(exercise(), 2))
158
+ strict = PureAnnoScript()
159
+ with self.assertRaisesRegex(ValueError, "check failed"):
160
+ evaluate(self.plan(checked_chain).to_dict(), strict, mode="strict", order="reverse")
161
+ self.assertEqual([x[0] for x in strict.calls], ["check"])
162
+
163
+ def test_first_observed_infer_failure_can_precede_pending_check(self):
164
+ class Concurrent(PureAnnoScript):
165
+ async def generate(self, instructions, **kwargs):
166
+ self.calls.append((instructions, kwargs))
167
+ if instructions == "check":
168
+ self.check_started.set()
169
+ try:
170
+ await asyncio.Event().wait()
171
+ except asyncio.CancelledError:
172
+ self.cancelled = True
173
+ raise
174
+ await self.check_started.wait()
175
+ raise ValueError("independent failed first")
176
+
177
+ async def exercise():
178
+ anno = Concurrent()
179
+ anno.check_started, anno.cancelled = asyncio.Event(), False
180
+ executor = _Executor(self.plan(checked_chain).to_dict(), anno, "eager", "random")
181
+ with self.assertRaisesRegex(ValueError, "independent failed first"):
182
+ await executor.run()
183
+ self.assertTrue(anno.cancelled)
184
+ self.assertCountEqual([x[0] for x in anno.calls], ["check", "independent"])
185
+
186
+ asyncio.run(asyncio.wait_for(exercise(), 2))
187
+
188
+ def test_success_waits_for_root_nested_and_loop_checks(self):
189
+ class Delayed(PureAnnoScript):
190
+ async def generate(self, instructions, **kwargs):
191
+ self.started.set()
192
+ await self.release.wait()
193
+ return GenerateResponse(self.response)
194
+
195
+ async def exercise(function, state, valid):
196
+ anno = Delayed(state)
197
+ anno.started, anno.release = asyncio.Event(), asyncio.Event()
198
+ anno.response = "ok" if valid else "bad"
199
+ executor = _Executor(self.plan(function).to_dict(), anno, "eager", "reverse")
200
+ task = asyncio.create_task(executor.run())
201
+ try:
202
+ await anno.started.wait()
203
+ self.assertEqual(executor.returns, ["done"]) # candidate only
204
+ self.assertFalse(task.done())
205
+ anno.release.set()
206
+ if valid:
207
+ self.assertEqual(await task, "done")
208
+ else:
209
+ with self.assertRaisesRegex(ValueError, "check failed"):
210
+ await task
211
+ finally:
212
+ task.cancel()
213
+ await asyncio.gather(task, return_exceptions=True)
214
+
215
+ for function, state in ((checked_constant_return, None), (checked_nested_return, [True]),
216
+ (checked_unused_loop, ["check", "check"])):
217
+ for valid in (False, True):
218
+ with self.subTest(function=function.__name__, valid=valid):
219
+ asyncio.run(asyncio.wait_for(exercise(function, state, valid), 2))
220
+
221
+ def test_data_and_branch_activation_still_gate_infer_and_check(self):
222
+ for mode in ("eager", "strict"):
223
+ for order in ("forward", "reverse", "random"):
224
+ anno = PureAnnoScript()
225
+ self.assertEqual(evaluate(self.plan(checked_data).to_dict(), anno, mode=mode, order=order), "first second")
226
+ self.assertEqual([x[0] for x in anno.calls], ["first", "first second"])
227
+ anno = PureAnnoScript()
228
+ self.assertEqual(evaluate(self.plan(checked_control).to_dict(), anno, mode=mode, order=order), "independent")
229
+ self.assertCountEqual([x[0] for x in anno.calls], ["predicate", "independent"])
230
+ self.assertEqual(evaluate(self.plan(checked_nested_return).to_dict(), PureAnnoScript([]), mode=mode, order=order), "done")
231
+ self.assertEqual(evaluate(self.plan(checked_unused_loop).to_dict(), PureAnnoScript([]), mode=mode, order=order), "done")
232
+
233
+ def test_memory_versions_diagnostics_and_rebinding_match_ir(self):
234
+ for function, state in ((checked_memory, None), (checked_container_truth, None), (checked_message, False),
235
+ (checked_rebinding, None), (checked_memory_loop, ["one", "two", "three"])):
236
+ program = compile_program(function)
237
+ failure, expected = None, None
238
+ try:
239
+ expected = evaluate_ir(program.to_dict(), PureAnnoScript(state))
240
+ except ValueError as exc:
241
+ failure = str(exc)
242
+ plan = lower_to_plan(program)
243
+ checks = {t.id for t in plan.tasks if isinstance(t, p.Check)}
244
+ if function is not checked_rebinding:
245
+ self.assertTrue(any(d.source in checks and d.kind == "memory" and d.hazard == "WAR"
246
+ for d in plan.dependencies_for()))
247
+ for mode in ("eager", "strict"):
248
+ for order in ("forward", "reverse", "random"):
249
+ with self.subTest(function=function.__name__, mode=mode, order=order):
250
+ if failure:
251
+ with self.assertRaises(ValueError) as caught:
252
+ evaluate(plan.to_dict(), PureAnnoScript(state), mode=mode, order=order)
253
+ self.assertEqual(str(caught.exception), failure)
254
+ else:
255
+ self.assertEqual(evaluate(plan.to_dict(), PureAnnoScript(state), mode=mode, order=order), expected)
256
+
257
+ def test_unoptimized_ir_retains_check_bindings_and_memory_reads(self):
258
+ for function, state in ((checked_memory, None), (checked_rebinding, None),
259
+ (checked_memory_loop, ["one", "two"])):
260
+ program = compile_program(function, optimize=False)
261
+ expected = evaluate_ir(program.to_dict(), PureAnnoScript(state))
262
+ plan = lower_to_plan(program)
263
+ for mode in ("eager", "strict"):
264
+ self.assertEqual(evaluate(plan.to_dict(), PureAnnoScript(state), mode=mode, order="reverse"), expected)
265
+
266
+ def test_shared_results_do_not_skip_checks(self):
267
+ plan = self.plan(checked_shared)
268
+ self.assertEqual(len(plan.requests), 1)
269
+ for mode in ("eager", "strict"):
270
+ for state in ("same", "wrong"):
271
+ anno = PureAnnoScript(state)
272
+ if state == "same":
273
+ self.assertEqual(evaluate(plan.to_dict(), anno, mode=mode, order="reverse"), ["same", "same"])
274
+ else:
275
+ with self.assertRaisesRegex(ValueError, "shared check failed"):
276
+ evaluate(plan.to_dict(), anno, mode=mode, order="reverse")
277
+ self.assertEqual(len(anno.calls), 1)
278
+
279
+ def test_checked_loop_allows_eager_infer_overlap_only(self):
280
+ class Concurrent(PureAnnoScript):
281
+ def __init__(self, state):
282
+ super().__init__(state)
283
+ self.active = self.peak = 0
284
+
285
+ async def generate(self, instructions, **kwargs):
286
+ self.calls.append((instructions, kwargs))
287
+ self.active += 1
288
+ self.peak = max(self.peak, self.active)
289
+ try:
290
+ await asyncio.sleep(0.01)
291
+ return GenerateResponse(instructions)
292
+ finally:
293
+ self.active -= 1
294
+
295
+ plan = self.plan(checked_loop)
296
+ for mode, peak in (("eager", 3), ("strict", 1)):
297
+ anno = Concurrent(["one", "two", "three"])
298
+ self.assertEqual(evaluate(plan.to_dict(), anno, mode=mode), anno.state)
299
+ self.assertEqual(anno.peak, peak)
300
+ self.assertEqual(anno.active, 0)
301
+ self.assertEqual(evaluate(plan.to_dict(), PureAnnoScript([])), [])
302
+
303
+
304
+ if __name__ == "__main__":
305
+ unittest.main()
@@ -0,0 +1,272 @@
1
+ """Dependency projection, safe offline formats, CLI and saved-plan checks."""
2
+
3
+ from dataclasses import replace
4
+ import io
5
+ import json
6
+ from pathlib import Path
7
+ import tempfile
8
+ import unittest
9
+ from unittest.mock import patch
10
+
11
+ from annoscript import ir
12
+ from annoscript.compiler import compile_program
13
+ from annoscript.inspect import main
14
+ from annoscript.anno import constrain
15
+ from annoscript.plan import ExecutionPlan
16
+ from annoscript.plan_lowering import lower_to_plan
17
+ from annoscript.plan_formatter import analyze_plan, format_plan, format_plan_html, format_plan_mermaid
18
+ from annoscript import test_plan as examples
19
+
20
+
21
+ ROOT = Path(__file__).resolve().parents[1]
22
+
23
+
24
+ def data_chain(anno):
25
+ a = anno.generate("first", max_length=512).text
26
+ b = anno.generate(a + " next", max_length=512).text
27
+ return b
28
+
29
+
30
+ def checked_arm(anno):
31
+ if anno.state:
32
+ a = anno.generate("first", max_length=512).text
33
+ constrain(a != "")
34
+ return anno.generate("after", max_length=512).text
35
+
36
+
37
+ def exclusive(anno):
38
+ if anno.state:
39
+ return anno.generate("yes", max_length=512).text
40
+ else:
41
+ return anno.generate("no", max_length=512).text
42
+
43
+
44
+ def true_recurrence(anno):
45
+ text = "seed"
46
+ for item in anno.state:
47
+ text = anno.generate(text, max_length=512).text
48
+ return text
49
+
50
+
51
+ def internal_loop(anno):
52
+ text = anno.generate("first", max_length=512).text
53
+ result = []
54
+ for item in anno.state:
55
+ result = result + [text + item]
56
+ return anno.generate(json.dumps(result), max_length=512).text
57
+
58
+
59
+ def loop_export(anno):
60
+ result = "initial"
61
+ for item in anno.state:
62
+ result = anno.generate(item, max_length=512).text
63
+ return anno.generate(result, max_length=512).text
64
+
65
+
66
+ def dynamic_prompt(anno):
67
+ return anno.generate(f"判断品牌 {anno.state}", max_length=512).text
68
+
69
+
70
+ def memory_path(anno):
71
+ data = {"text": "initial"}
72
+ alias = data
73
+ a = anno.generate(data["text"], max_length=512).text
74
+ alias["text"] = a
75
+ return anno.generate(data["text"], max_length=512).text
76
+
77
+
78
+ def empty(anno):
79
+ return None
80
+
81
+
82
+ class PlanProjectionTests(unittest.TestCase):
83
+ def report(self, function, mode="eager", raw=False):
84
+ return analyze_plan(lower_to_plan(compile_program(function, optimize=not raw)), mode=mode)
85
+
86
+ def test_independent_calls_and_strict_failure_order(self):
87
+ eager = self.report(examples.independent)
88
+ self.assertEqual(eager["edges"], [])
89
+ self.assertEqual(eager["batches"], [{"layer": 0, "operation": "generate", "sites": ["I01", "I02"]}])
90
+ strict = self.report(examples.independent, "strict")
91
+ self.assertEqual(strict["batches"], [])
92
+ self.assertEqual([(e["source"], e["target"]) for e in strict["edges"]], [("I01", "I02")])
93
+ self.assertIn("failure_order", strict["edges"][0]["kinds"])
94
+
95
+ def test_data_path_projects_through_pure_tasks(self):
96
+ for raw in (False, True):
97
+ report = self.report(data_chain, raw=raw)
98
+ edge, = report["edges"]
99
+ self.assertEqual((edge["source"], edge["target"], edge["carried"]), ("I01", "I02", False))
100
+ self.assertIn("data", edge["kinds"])
101
+ self.assertGreater(len(edge["witness"]), 2)
102
+ for left, right in zip(edge["witness"], edge["witness"][1:]):
103
+ self.assertEqual(left["target"], right["source"])
104
+ self.assertEqual(report["parallel"], [])
105
+
106
+ def test_join_completion_and_constraint_barrier(self):
107
+ report = self.report(checked_arm)
108
+ self.assertEqual(report["edges"], [])
109
+ self.assertEqual(len(report["checks"]), 1)
110
+ self.assertEqual(report["parallel"][0]["sites"], ["I01", "I02"])
111
+ report = self.report(checked_arm, "strict")
112
+ edge, = report["edges"]
113
+ self.assertIn("completion", edge["kinds"])
114
+ self.assertIn("constraint", edge["kinds"])
115
+ self.assertTrue(edge["guards"])
116
+ self.assertEqual(len(report["checks"]), 1)
117
+ self.assertEqual(report["parallel"], [])
118
+
119
+ def test_memory_path_preserves_hazards_and_certainty(self):
120
+ report = self.report(memory_path)
121
+ edge, = report["edges"]
122
+ self.assertIn("memory", edge["kinds"])
123
+ self.assertIn("RAW", edge["hazards"])
124
+ self.assertTrue(edge["conservative"])
125
+ self.assertEqual(report["parallel"], [])
126
+
127
+ def test_mutually_exclusive_sites_are_not_a_batch(self):
128
+ report = self.report(exclusive)
129
+ self.assertEqual(report["edges"], [])
130
+ self.assertEqual(report["parallel"], [])
131
+ self.assertEqual(report["batches"], [])
132
+ self.assertTrue(all(n["gates"] for n in report["nodes"]))
133
+
134
+ def test_result_accumulation_is_not_an_infer_recurrence(self):
135
+ report = self.report(examples.independent_loop)
136
+ self.assertEqual(report["edges"], [])
137
+ loop, = report["loops"]
138
+ self.assertTrue(loop["parallel_iterations"])
139
+ self.assertTrue(loop["carried_values"])
140
+ self.assertEqual(loop["batch_sites"], ["I01"])
141
+ strict = self.report(examples.independent_loop, "strict")
142
+ self.assertFalse(strict["loops"][0]["parallel_iterations"])
143
+ self.assertTrue(any(e["carried"] and e["source"] == e["target"] for e in strict["edges"]))
144
+
145
+ def test_true_recurrence_blocks_cross_iteration_batching(self):
146
+ report = self.report(true_recurrence)
147
+ edge, = report["edges"]
148
+ self.assertEqual((edge["source"], edge["target"], edge["carried"]), ("I01", "I01", True))
149
+ self.assertIn({"loop": "L01", "distance": 1}, edge["recurrences"])
150
+ self.assertEqual(report["loops"][0]["batch_sites"], [])
151
+ self.assertFalse(report["loops"][0]["parallel_iterations"])
152
+
153
+ def test_internal_pure_loop_is_gathered_not_an_infer_recurrence(self):
154
+ report = self.report(internal_loop)
155
+ edge, = report["edges"]
156
+ self.assertFalse(edge["carried"])
157
+ self.assertEqual(edge["recurrences"], [])
158
+ self.assertEqual(edge["via_loops"], ["L01"])
159
+ self.assertEqual(report["unresolved_cycles"], [])
160
+
161
+ def test_loop_exit_gathers_and_nested_coordinates_are_retained(self):
162
+ report = self.report(loop_export)
163
+ edge, = report["edges"]
164
+ self.assertFalse(edge["carried"])
165
+ self.assertEqual(edge["gather"], ["L01"])
166
+ nested = self.report(examples.nested_loop, "strict")
167
+ self.assertEqual(nested["nodes"][0]["loops"], ["L01", "L02"])
168
+ loops = {r["loop"] for e in nested["edges"] for r in e["recurrences"]}
169
+ self.assertEqual(loops, {"L01", "L02"})
170
+
171
+ def test_shared_requests_are_not_counted_as_batch_work(self):
172
+ report = self.report(examples.duplicates)
173
+ self.assertEqual(report["nodes"][0]["request"], report["nodes"][1]["request"])
174
+ self.assertEqual(report["batches"], [])
175
+ invariant = self.report(examples.loop_invariant)
176
+ self.assertEqual(invariant["loops"][0]["shared_sites"], ["I01"])
177
+ self.assertEqual(invariant["loops"][0]["batch_sites"], [])
178
+
179
+ def test_loop_check_only_enforces_strict_iteration_order(self):
180
+ report = self.report(examples.checked_loop)
181
+ self.assertFalse(report["loops"][0]["serial"])
182
+ self.assertTrue(report["loops"][0]["parallel_iterations"])
183
+ self.assertEqual(report["edges"], [])
184
+ strict = self.report(examples.checked_loop, "strict")
185
+ self.assertTrue(strict["loops"][0]["serial"])
186
+ self.assertTrue(any("iteration_order" in e["kinds"] for e in strict["edges"]))
187
+
188
+ def test_serialization_determinism_and_source_independence(self):
189
+ plan = lower_to_plan(compile_program(dynamic_prompt))
190
+ before = plan.to_dict()
191
+ restored = ExecutionPlan.from_json(plan.to_json())
192
+ with patch("annoscript.compiler.compile_program", side_effect=AssertionError("no compilation")), patch(
193
+ "annoscript.plan_lowering.lower_to_plan", side_effect=AssertionError("no lowering")
194
+ ):
195
+ report = analyze_plan(restored)
196
+ for formatter in (format_plan, format_plan_mermaid, format_plan_html):
197
+ self.assertEqual(formatter(plan), formatter(restored))
198
+ self.assertEqual(plan.to_dict(), before)
199
+ self.assertIn("判断品牌 {", report["nodes"][0]["label"])
200
+ self.assertIn("format_string", report["nodes"][0]["prompt"])
201
+ self.assertIn("annoscript.test_plan_formatter:", format_plan(plan, show_source=True))
202
+ report["nodes"][0]["label"] = "modified"
203
+ self.assertNotEqual(report, analyze_plan(plan))
204
+
205
+ def test_no_infer_and_invalid_mode(self):
206
+ plan = lower_to_plan(compile_program(empty))
207
+ self.assertEqual(analyze_plan(plan)["nodes"], [])
208
+ self.assertIn("0 Infer sites", format_plan(plan))
209
+ self.assertIn('"nodes": []', format_plan_html(plan))
210
+ with self.assertRaises(ValueError):
211
+ analyze_plan(plan, mode="invalid")
212
+
213
+
214
+ class PlanFormatCommandTests(unittest.TestCase):
215
+ def setUp(self):
216
+ self.plan = lower_to_plan(compile_program(examples.independent))
217
+
218
+ def command(self, *args, text=None):
219
+ stdout, stderr = io.StringIO(), io.StringIO()
220
+ with patch("sys.stdin", io.StringIO(text or self.plan.to_json())), patch("sys.stdout", stdout), patch("sys.stderr", stderr):
221
+ status = main(list(args))
222
+ return status, stdout.getvalue(), stderr.getvalue()
223
+
224
+ def test_stdin_formats_and_mode(self):
225
+ for fmt, formatter in (("text", format_plan), ("mermaid", format_plan_mermaid), ("html", format_plan_html)):
226
+ status, out, err = self.command("-", "--format", fmt, "--mode", "strict", "--show-source")
227
+ self.assertEqual((status, err), (0, ""))
228
+ self.assertEqual(out, formatter(self.plan, mode="strict", show_source=True))
229
+
230
+ def test_file_output_and_no_overwriting_input(self):
231
+ with tempfile.TemporaryDirectory() as directory:
232
+ source, dest = Path(directory) / "输入.json", Path(directory) / "图.html"
233
+ source.write_text(self.plan.to_json(), encoding="utf-8")
234
+ self.assertEqual(self.command(str(source), "--format", "html", "-o", str(dest)), (0, "", ""))
235
+ self.assertEqual(dest.read_text(encoding="utf-8"), format_plan_html(self.plan))
236
+ before = source.read_bytes()
237
+ status, out, err = self.command(str(source), "-o", str(source))
238
+ self.assertEqual((status, out), (2, ""))
239
+ self.assertIn("must differ", err)
240
+ self.assertEqual(source.read_bytes(), before)
241
+
242
+ def test_errors_use_strict_decoder_and_do_not_lower_ir(self):
243
+ invalid = self.plan.to_dict()
244
+ invalid["tasks"][0]["region"] = "missing"
245
+ for text in (json.dumps(invalid), self.plan.to_json().replace('"version": 1', '"version": 1, "version": 1'),
246
+ '{"format":"lev-execution-plan","version":NaN}'):
247
+ status, out, err = self.command("-", text=text)
248
+ self.assertEqual((status, out), (2, ""))
249
+ self.assertIn("annoscript.inspect:", err)
250
+ self.assertNotIn("Traceback", err)
251
+ status, out, err = self.command("-", "--format", "html", text=compile_program(empty).to_json())
252
+ self.assertEqual((status, out), (2, ""))
253
+ self.assertIn("lower the IR explicitly first", err)
254
+
255
+ def test_untrusted_strings_cannot_inject_html_or_mermaid(self):
256
+ program = compile_program(empty)
257
+ prompt = '中文</script><script>alert(1)</script>"\nclick I01 "https://example.com" __PLAN_DATA__'
258
+ statement = ir.Infer("reply", "generate", (ir.Literal(prompt),), (("max_length",ir.Literal(512)),), id="attack", source=ir.Source(program.source))
259
+ plan = lower_to_plan(replace(program, entry=prompt, body=(statement,) + program.body))
260
+ html = format_plan_html(plan)
261
+ self.assertEqual(html.count("<script"), 2)
262
+ self.assertEqual(html.count("</script>"), 2)
263
+ self.assertNotIn("<script>alert(1)", html)
264
+ payload = html.split('<script id="plan-data" type="application/json">', 1)[1].split("</script>", 1)[0]
265
+ self.assertEqual(json.loads(payload)["eager"]["nodes"][0]["prompt"], prompt)
266
+ mermaid = format_plan_mermaid(plan)
267
+ self.assertNotIn("<script", mermaid)
268
+ self.assertNotIn('\nclick ', mermaid)
269
+
270
+
271
+ if __name__ == "__main__":
272
+ unittest.main()