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,953 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>__PLAN_TITLE__ · AnnoScript Execution Plan</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --ink: #172635;
11
+ --muted: #617286;
12
+ --line: #dce4eb;
13
+ --paper: #fff;
14
+ --accent: #006d77;
15
+ }
16
+ * {
17
+ box-sizing: border-box;
18
+ }
19
+ body {
20
+ margin: 0;
21
+ font:
22
+ 14px/1.6 system-ui,
23
+ -apple-system,
24
+ "Segoe UI",
25
+ sans-serif;
26
+ color: var(--ink);
27
+ background: #f5f7fa;
28
+ height: 100vh;
29
+ display: flex;
30
+ flex-direction: column;
31
+ }
32
+ header {
33
+ padding: 20px 28px 15px;
34
+ background: var(--paper);
35
+ border-bottom: 1px solid var(--line);
36
+ }
37
+ .eyebrow {
38
+ font-size: 11px;
39
+ letter-spacing: 2px;
40
+ font-weight: 700;
41
+ color: var(--accent);
42
+ }
43
+ h1 {
44
+ font-size: 23px;
45
+ line-height: 1.4;
46
+ margin: 5px 0;
47
+ }
48
+ header p {
49
+ margin: 0;
50
+ color: var(--muted);
51
+ font-size: 13px;
52
+ }
53
+ .toolbar {
54
+ display: flex;
55
+ gap: 12px;
56
+ align-items: center;
57
+ flex-wrap: wrap;
58
+ padding: 12px 24px;
59
+ border-bottom: 1px solid var(--line);
60
+ background: var(--paper);
61
+ }
62
+ button,
63
+ select,
64
+ input {
65
+ font: inherit;
66
+ }
67
+ button,
68
+ select {
69
+ border: 1px solid #cbd6df;
70
+ border-radius: 6px;
71
+ background: white;
72
+ padding: 5px 10px;
73
+ color: var(--ink);
74
+ }
75
+ button {
76
+ cursor: pointer;
77
+ }
78
+ button:hover,
79
+ select:hover {
80
+ border-color: var(--accent);
81
+ }
82
+ button:focus-visible,
83
+ input:focus-visible,
84
+ select:focus-visible {
85
+ outline: 3px solid #83c5be;
86
+ outline-offset: 2px;
87
+ }
88
+ .toolbar label {
89
+ display: flex;
90
+ gap: 6px;
91
+ align-items: center;
92
+ font-size: 12px;
93
+ }
94
+ .toolbar input[type="search"] {
95
+ padding: 6px 9px;
96
+ border: 1px solid #cbd6df;
97
+ border-radius: 6px;
98
+ width: 170px;
99
+ }
100
+ .spacer {
101
+ flex: 1;
102
+ }
103
+ .workspace {
104
+ display: grid;
105
+ grid-template-columns: minmax(0, 1fr) 330px;
106
+ flex: 1;
107
+ min-height: 0;
108
+ }
109
+ .graph-pane {
110
+ min-height: 0;
111
+ display: flex;
112
+ flex-direction: column;
113
+ }
114
+ .legend {
115
+ padding: 9px 24px;
116
+ font-size: 11px;
117
+ display: flex;
118
+ gap: 18px;
119
+ color: var(--muted);
120
+ border-bottom: 1px solid var(--line);
121
+ }
122
+ .legend i {
123
+ display: inline-block;
124
+ width: 20px;
125
+ border-top: 2px solid;
126
+ vertical-align: middle;
127
+ margin-right: 5px;
128
+ }
129
+ .canvas {
130
+ overflow: auto;
131
+ flex: 1;
132
+ min-height: 0;
133
+ padding: 10px 18px 36px;
134
+ background-image: radial-gradient(#d9e1e8 0.65px, transparent 0.65px);
135
+ background-size: 16px 16px;
136
+ }
137
+ .canvas svg {
138
+ display: block;
139
+ margin: auto;
140
+ overflow: visible;
141
+ }
142
+ aside {
143
+ border-left: 1px solid var(--line);
144
+ background: var(--paper);
145
+ padding: 20px;
146
+ overflow: auto;
147
+ }
148
+ h2 {
149
+ font-size: 17px;
150
+ margin: 0 0 12px;
151
+ }
152
+ h3 {
153
+ font-size: 13px;
154
+ margin: 22px 0 8px;
155
+ }
156
+ .muted {
157
+ color: var(--muted);
158
+ font-size: 12px;
159
+ }
160
+ .stats {
161
+ display: grid;
162
+ grid-template-columns: repeat(3, 1fr);
163
+ gap: 8px;
164
+ margin-bottom: 18px;
165
+ }
166
+ .stat {
167
+ background: #f1f5f8;
168
+ border-radius: 8px;
169
+ padding: 9px 5px;
170
+ text-align: center;
171
+ }
172
+ .stat strong {
173
+ display: block;
174
+ font-size: 22px;
175
+ line-height: 1.3;
176
+ }
177
+ .stat span {
178
+ font-size: 10px;
179
+ color: var(--muted);
180
+ }
181
+ .card {
182
+ border: 1px solid var(--line);
183
+ border-radius: 8px;
184
+ padding: 12px;
185
+ margin: 9px 0;
186
+ }
187
+ .card-title {
188
+ font-size: 12px;
189
+ font-weight: 650;
190
+ margin-bottom: 7px;
191
+ }
192
+ .chips {
193
+ display: flex;
194
+ gap: 5px;
195
+ flex-wrap: wrap;
196
+ }
197
+ .chip {
198
+ font-size: 11px;
199
+ padding: 2px 7px;
200
+ color: var(--accent);
201
+ background: #edf8f6;
202
+ border-color: #c5e4df;
203
+ }
204
+ .badge {
205
+ display: inline-block;
206
+ border-radius: 4px;
207
+ padding: 1px 5px;
208
+ font-size: 10px;
209
+ background: #e8f5f0;
210
+ color: #15624e;
211
+ }
212
+ .badge.warn {
213
+ background: #fff3de;
214
+ color: #96611c;
215
+ }
216
+ .note {
217
+ font-size: 11px;
218
+ color: var(--muted);
219
+ padding: 9px 0;
220
+ border-top: 1px solid #edf1f4;
221
+ }
222
+ pre {
223
+ font:
224
+ 11px/1.6 ui-monospace,
225
+ SFMono-Regular,
226
+ Consolas,
227
+ monospace;
228
+ white-space: pre-wrap;
229
+ overflow-wrap: anywhere;
230
+ background: #f4f7f9;
231
+ padding: 10px;
232
+ border-radius: 6px;
233
+ margin: 8px 0;
234
+ }
235
+ .node {
236
+ cursor: pointer;
237
+ outline: none;
238
+ }
239
+ .node:focus rect {
240
+ stroke: #006d77;
241
+ stroke-width: 3;
242
+ }
243
+ .node rect {
244
+ transition:
245
+ fill 0.12s,
246
+ stroke 0.12s;
247
+ }
248
+ .node .id {
249
+ font-size: 11px;
250
+ font-weight: 750;
251
+ }
252
+ .node .label {
253
+ font-size: 12px;
254
+ fill: #253646;
255
+ }
256
+ .node .meta {
257
+ font-size: 10px;
258
+ fill: #748396;
259
+ }
260
+ .node.selected rect {
261
+ stroke: #007f86;
262
+ stroke-width: 3;
263
+ fill: #eaf8f5;
264
+ }
265
+ .node.related rect {
266
+ stroke: #007f86;
267
+ stroke-width: 2;
268
+ }
269
+ .node.dim {
270
+ opacity: 0.25;
271
+ }
272
+ .edge {
273
+ fill: none;
274
+ stroke-width: 1.5;
275
+ stroke-linejoin: round;
276
+ opacity: 0.7;
277
+ }
278
+ .edge.carried {
279
+ stroke-dasharray: 5 4;
280
+ }
281
+ .edge.focused {
282
+ stroke-width: 2.5;
283
+ opacity: 1;
284
+ }
285
+ .edge.dim {
286
+ opacity: 0.1;
287
+ }
288
+ .edge-hit {
289
+ fill: none;
290
+ stroke: transparent;
291
+ stroke-width: 13;
292
+ cursor: pointer;
293
+ }
294
+ .stage {
295
+ fill: #fff;
296
+ fill-opacity: 0.58;
297
+ stroke: #e3eaf0;
298
+ stroke-dasharray: 3 4;
299
+ }
300
+ .stage-title {
301
+ font-size: 10px;
302
+ fill: #718194;
303
+ }
304
+ .empty {
305
+ padding: 40px;
306
+ text-align: center;
307
+ color: var(--muted);
308
+ }
309
+ details {
310
+ margin-top: 10px;
311
+ }
312
+ summary {
313
+ cursor: pointer;
314
+ font-size: 12px;
315
+ }
316
+ #status {
317
+ font-size: 11px;
318
+ color: var(--muted);
319
+ }
320
+ @media (max-width: 850px) {
321
+ .workspace {
322
+ grid-template-columns: minmax(0, 1fr) 285px;
323
+ }
324
+ .toolbar {
325
+ gap: 8px;
326
+ padding: 10px;
327
+ }
328
+ header {
329
+ padding: 14px 18px;
330
+ }
331
+ .legend {
332
+ gap: 9px;
333
+ padding: 8px;
334
+ font-size: 10px;
335
+ }
336
+ aside {
337
+ padding: 13px;
338
+ }
339
+ }
340
+ @media (max-width: 620px) {
341
+ body {
342
+ height: auto;
343
+ min-height: 100vh;
344
+ }
345
+ .workspace {
346
+ display: flex;
347
+ flex-direction: column;
348
+ }
349
+ .canvas {
350
+ height: 65vh;
351
+ min-height: 380px;
352
+ }
353
+ .graph-pane {
354
+ flex: none;
355
+ }
356
+ aside {
357
+ border-left: 0;
358
+ border-top: 1px solid var(--line);
359
+ overflow: visible;
360
+ }
361
+ .legend {
362
+ flex-wrap: wrap;
363
+ }
364
+ .spacer {
365
+ display: none;
366
+ }
367
+ .toolbar input[type="search"] {
368
+ width: 135px;
369
+ }
370
+ }
371
+ </style>
372
+ </head>
373
+ <body>
374
+ <header>
375
+ <div class="eyebrow">LEV / EXECUTION PLAN</div>
376
+ <h1>
377
+ __PLAN_TITLE__
378
+ <span style="font-weight: 400; color: #81909e">· Infer 依赖</span>
379
+ </h1>
380
+ <p>
381
+ 沿着依赖找到并行与批处理机会。图中节点是调用位置,实际就绪状态由运行时决定。
382
+ </p>
383
+ </header>
384
+ <div class="toolbar">
385
+ <label
386
+ >失败模式
387
+ <select id="mode" aria-label="失败模式">
388
+ <option value="eager">eager · 允许失败前并发</option>
389
+ <option value="strict">strict · 保留失败顺序</option>
390
+ <option value="speculative">speculative · 先决策,再按路径生成</option>
391
+ </select></label
392
+ >
393
+ <label><input type="checkbox" id="transitive" />传递边</label
394
+ ><label><input type="checkbox" id="carried" checked />跨迭代边</label>
395
+ <input
396
+ id="search"
397
+ type="search"
398
+ placeholder="搜索调用 / 指令"
399
+ aria-label="搜索调用或指令"
400
+ />
401
+ <span class="spacer"></span><button id="minus" aria-label="缩小">−</button
402
+ ><span id="zoom">100%</span><button id="plus" aria-label="放大">+</button
403
+ ><button id="fit">适合宽度</button><button id="reset">全部机会</button>
404
+ </div>
405
+ <div class="workspace">
406
+ <section class="graph-pane" aria-label="依赖图">
407
+ <div class="legend">
408
+ <span><i style="color: #7294b4"></i>数据 / 控制</span
409
+ ><span><i style="color: #c58a39"></i>约束顺序依赖</span
410
+ ><span><i style="color: #c87373"></i>严格顺序</span
411
+ ><span
412
+ ><i style="color: #8565b0; border-top-style: dashed"></i
413
+ >跨迭代</span
414
+ ><span id="status"></span>
415
+ </div>
416
+ <div class="canvas" id="canvas">
417
+ <svg id="graph" role="img" aria-label="Infer 依赖图"></svg>
418
+ </div>
419
+ </section>
420
+ <aside id="details" aria-live="polite"></aside>
421
+ </div>
422
+ <script id="plan-data" type="application/json">
423
+ __PLAN_DATA__
424
+ </script>
425
+ <script>
426
+ "use strict";
427
+ const DATA = JSON.parse(document.getElementById("plan-data").textContent);
428
+ const $ = (id) => document.getElementById(id),
429
+ NS = "http://www.w3.org/2000/svg";
430
+ const color = {
431
+ choice: "#3473aa",
432
+ generate: "#007f75",
433
+ extract: "#7c5fa7",
434
+ generate_list: "#397f50",
435
+ score: "#a27238",
436
+ noul: "#8a5d73",
437
+ };
438
+ let mode = DATA.initial_mode,
439
+ report = DATA[mode],
440
+ selected = [],
441
+ positions = {},
442
+ scale = 1,
443
+ size = { width: 850, height: 300 };
444
+ $("mode").value = mode;
445
+ function el(tag, text, parent, cls) {
446
+ const n = document.createElement(tag);
447
+ if (text !== undefined) n.textContent = text;
448
+ if (cls) n.className = cls;
449
+ if (parent) parent.append(n);
450
+ return n;
451
+ }
452
+ function svg(tag, attrs = {}, text) {
453
+ const n = document.createElementNS(NS, tag);
454
+ for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
455
+ if (text !== undefined) n.textContent = text;
456
+ return n;
457
+ }
458
+ function block(title, text, parent = $("details")) {
459
+ el("h3", title, parent);
460
+ return el("pre", text, parent);
461
+ }
462
+ function chips(sites, parent) {
463
+ const row = el("div", undefined, parent, "chips");
464
+ for (const id of sites) {
465
+ const b = el("button", id, row, "chip");
466
+ b.onclick = () => select([id], true);
467
+ }
468
+ return row;
469
+ }
470
+ function cards(groups, label, parent) {
471
+ if (!groups.length)
472
+ el("p", "当前模式未发现此类静态候选。", parent, "muted");
473
+ for (const g of groups) {
474
+ const box = el("div", undefined, parent, "card");
475
+ el("div", label(g), box, "card-title");
476
+ chips(g.sites, box);
477
+ const b = el("button", "定位这一组", box, "chip");
478
+ b.style.marginTop = "8px";
479
+ b.onclick = () => select(g.sites, true);
480
+ }
481
+ }
482
+ function opportunities() {
483
+ const panel = $("details");
484
+ panel.replaceChildren();
485
+ el("h2", "可调度的机会", panel);
486
+ const stats = el("div", undefined, panel, "stats");
487
+ for (const [v, t] of [
488
+ [report.stats.infer_sites, "Infer 位置"],
489
+ [report.batches.length, "同算子候选组"],
490
+ [
491
+ report.loops.filter((l) => l.parallel_iterations).length,
492
+ "可并行循环",
493
+ ],
494
+ ]) {
495
+ const s = el("div", undefined, stats, "stat");
496
+ el("strong", String(v), s);
497
+ el("span", t, s);
498
+ }
499
+ el(
500
+ "p",
501
+ mode === "speculative"
502
+ ? "图中保留正式依赖。每条输入先完成本轮 Jev 决策(允许跨未决分支推测),再合批已确认路径的 Qwen 任务;交替依赖按轮执行。推测候选:" + report.speculative_candidates.join(", ")
503
+ : mode === "eager"
504
+ ? "独立请求可越过待完成的约束检查;成功返回仍须通过全部检查,失败时可能已有额外调用发出。"
505
+ : "保留失败顺序;本 request 的调用通常需要等待前序操作。",
506
+ panel,
507
+ "muted",
508
+ );
509
+ el("h3", "同算子批处理候选", panel);
510
+ cards(
511
+ report.batches,
512
+ (g) => `${g.operation} · 依赖层 ${g.layer}`,
513
+ panel,
514
+ );
515
+ el("h3", "跨迭代调用", panel);
516
+ for (const loop of report.loops.filter((l) => l.sites.length)) {
517
+ const box = el("div", undefined, panel, "card");
518
+ el(
519
+ "div",
520
+ `${loop.id} · ${loop.parallel_iterations ? "可跨迭代并行" : "存在迭代顺序限制"}`,
521
+ box,
522
+ "card-title",
523
+ );
524
+ chips(loop.sites, box);
525
+ el("p", loop.iterable, box, "muted");
526
+ if (loop.batch_sites.length)
527
+ el("span", "跨迭代批处理候选", box, "badge");
528
+ if (loop.shared_sites.length)
529
+ el(
530
+ "p",
531
+ `共享调用 ${loop.shared_sites.join(", ")}:复用结果,不作为多次请求合批。`,
532
+ box,
533
+ "muted",
534
+ );
535
+ if (loop.carried_values.length)
536
+ el(
537
+ "p",
538
+ `携带值:${loop.carried_values.join(", ")}。仅有累积依赖不代表 Infer 必须串行。`,
539
+ box,
540
+ "muted",
541
+ );
542
+ }
543
+ const more = el("details", undefined, panel);
544
+ el("summary", "查看跨算子的并行候选", more);
545
+ cards(report.parallel, (g) => `依赖层 ${g.layer}`, more);
546
+ const checks = el("details", undefined, panel);
547
+ el(
548
+ "summary",
549
+ `约束检查 · ${report.checks.length}${mode === "strict" ? " · 顺序屏障" : " · 成功返回前必须通过"}`,
550
+ checks,
551
+ );
552
+ for (const c of report.checks) block(c.id, c.condition, checks);
553
+ if (report.unresolved_cycles.length)
554
+ el(
555
+ "p",
556
+ `未解析同轮环:${report.unresolved_cycles.join(", ")}。不据此推断并行。`,
557
+ panel,
558
+ "note",
559
+ );
560
+ for (const note of report.notes) el("p", note, panel, "note");
561
+ }
562
+ function showNode(id) {
563
+ const n = report.nodes.find((n) => n.id === id),
564
+ panel = $("details");
565
+ panel.replaceChildren();
566
+ const back = el("button", "← 全部机会", panel);
567
+ back.onclick = () => select([]);
568
+ el("h2", `${n.id} · ${n.operation}`, panel);
569
+ el("p", n.label, panel, "muted");
570
+ block("完整指令(符号表示)", n.prompt);
571
+ block(
572
+ "激活条件",
573
+ n.gates.join("\n") || "无分支条件;仍需等待输入计算与前置依赖。",
574
+ );
575
+ block(
576
+ "请求与作用域",
577
+ `${n.request} · ${n.reusable ? "允许共享" : "不共享"}\n循环:${n.loops.join(", ") || "无"}\n复用作用域:${n.reuse_scope.join(" / ") || "request"}`,
578
+ );
579
+ const peers = report.nodes
580
+ .filter((x) => x.request === n.request && x.id !== n.id)
581
+ .map((x) => x.id);
582
+ if (peers.length) {
583
+ el("h3", "共享此请求的其他位置", panel);
584
+ chips(peers, panel);
585
+ }
586
+ el("h3", "上游 Infer(含条件路径)", panel);
587
+ chips(
588
+ [
589
+ ...new Set(
590
+ report.edges.filter((e) => e.target === id).map((e) => e.source),
591
+ ),
592
+ ],
593
+ panel,
594
+ );
595
+ el("h3", "下游 Infer", panel);
596
+ chips(
597
+ [
598
+ ...new Set(
599
+ report.edges.filter((e) => e.source === id).map((e) => e.target),
600
+ ),
601
+ ],
602
+ panel,
603
+ );
604
+ const args = el("details", undefined, panel);
605
+ el("summary", "参数与关键词", args);
606
+ block("(位置参数, 关键词值)", n.arguments, args);
607
+ block("关键词顺序", n.keywords.join(", "), args);
608
+ const source = el("details", undefined, panel);
609
+ source.open = DATA.show_source;
610
+ el("summary", "来源与原始标识", source);
611
+ block(
612
+ "来源",
613
+ [
614
+ n.task,
615
+ n.origin,
616
+ n.source,
617
+ ...n.calls.map((c) => "inlined from " + c),
618
+ ].join("\n"),
619
+ source,
620
+ );
621
+ }
622
+ function showEdge(e) {
623
+ const panel = $("details");
624
+ panel.replaceChildren();
625
+ const back = el("button", "← 全部机会", panel);
626
+ back.onclick = () => select([]);
627
+ el("h2", `${e.source} → ${e.target}`, panel);
628
+ block(
629
+ "依赖原因",
630
+ e.kinds.join(" / ") +
631
+ (e.conservative ? "\n含保守的可能依赖" : "") +
632
+ (e.hazards.length ? "\n" + e.hazards.join(" / ") : ""),
633
+ );
634
+ block(
635
+ "实例关系",
636
+ [
637
+ e.carried ? "存在跨迭代 Infer 依赖" : "同一公共迭代坐标内的依赖",
638
+ e.gather.length ? "汇合:" + e.gather.join(", ") : "",
639
+ e.broadcast.length ? "广播:" + e.broadcast.join(", ") : "",
640
+ ...e.recurrences.map(
641
+ (r) => `原始循环边 ${r.loop}: distance=${r.distance}`,
642
+ ),
643
+ e.via_loops.length
644
+ ? "路径经过的内部循环:" + e.via_loops.join(", ")
645
+ : "",
646
+ ]
647
+ .filter(Boolean)
648
+ .join("\n"),
649
+ );
650
+ if (e.guards.length)
651
+ block("可能路径上的条件(不是一个合取条件)", e.guards.join("\n"));
652
+ el(
653
+ "p",
654
+ "下方是一条依赖路径见证;原因和条件汇总可能包含其他路径。跨多条循环边时,不将其简化为单一距离。",
655
+ panel,
656
+ "muted",
657
+ );
658
+ block(
659
+ "路径见证",
660
+ e.witness
661
+ .map(
662
+ (w) =>
663
+ `${w.source.join(".")} → ${w.target.join(".")}${w.kind ? " [" + w.kind + "]" : ""}${w.loop ? " " + w.loop + " +" + w.distance : ""}`,
664
+ )
665
+ .join("\n"),
666
+ );
667
+ }
668
+ function select(ids, jump = false) {
669
+ selected = ids;
670
+ highlight();
671
+ if (ids.length === 1) showNode(ids[0]);
672
+ else if (ids.length > 1) {
673
+ opportunities();
674
+ el("p", "已高亮:" + ids.join(", "), $("details"), "note");
675
+ } else opportunities();
676
+ if (jump && ids.length) {
677
+ const pos = positions[ids[0]];
678
+ $("canvas").scrollTo({
679
+ top: Math.max(0, pos.y * scale - 45),
680
+ behavior: "smooth",
681
+ });
682
+ }
683
+ }
684
+ function visibleEdges() {
685
+ return report.edges.filter(
686
+ (e) =>
687
+ ($("transitive").checked || !e.redundant) &&
688
+ ($("carried").checked || !e.carried),
689
+ );
690
+ }
691
+ function highlight() {
692
+ const term = $("search").value.trim().toLowerCase(),
693
+ near = new Set(selected);
694
+ for (const e of report.edges)
695
+ if (selected.includes(e.source) || selected.includes(e.target)) {
696
+ near.add(e.source);
697
+ near.add(e.target);
698
+ }
699
+ for (const n of report.nodes) {
700
+ const g = $("node-" + n.id),
701
+ match =
702
+ !term ||
703
+ `${n.id} ${n.operation} ${n.prompt}`.toLowerCase().includes(term);
704
+ g.classList.toggle("selected", selected.includes(n.id));
705
+ g.classList.toggle("related", near.has(n.id));
706
+ g.classList.toggle(
707
+ "dim",
708
+ !match || (selected.length > 0 && !near.has(n.id)),
709
+ );
710
+ }
711
+ for (const path of document.querySelectorAll(".edge")) {
712
+ const active =
713
+ selected.includes(path.dataset.source) ||
714
+ selected.includes(path.dataset.target);
715
+ path.classList.toggle("focused", active);
716
+ path.classList.toggle("dim", selected.length > 0 && !active);
717
+ }
718
+ }
719
+ function zoom() {
720
+ const graph = $("graph");
721
+ graph.setAttribute("width", size.width * scale);
722
+ graph.setAttribute("height", size.height * scale);
723
+ $("zoom").textContent = Math.round(scale * 100) + "%";
724
+ }
725
+ function fit() {
726
+ scale = Math.min(
727
+ 1,
728
+ Math.max(0.25, ($("canvas").clientWidth - 38) / size.width),
729
+ );
730
+ zoom();
731
+ }
732
+ function draw() {
733
+ const graph = $("graph");
734
+ graph.replaceChildren();
735
+ positions = {};
736
+ const layers = report.layers.map((a) => [...a]);
737
+ if (report.unresolved_cycles.length)
738
+ layers.push(report.unresolved_cycles);
739
+ const W = 224,
740
+ H = 100,
741
+ G = 27,
742
+ WIDTH = 850;
743
+ let y = 42;
744
+ for (let i = 0; i < layers.length; i++) {
745
+ const members = layers[i],
746
+ rows = Math.ceil(members.length / 3),
747
+ height = rows * (H + G) + 25;
748
+ graph.append(
749
+ svg("rect", {
750
+ x: 5,
751
+ y: y - 25,
752
+ width: WIDTH - 10,
753
+ height: height,
754
+ rx: 12,
755
+ class: "stage",
756
+ }),
757
+ );
758
+ graph.append(
759
+ svg(
760
+ "text",
761
+ { x: 21, y: y - 9, class: "stage-title" },
762
+ `依赖层 ${i} · ${members.length} 个位置`,
763
+ ),
764
+ );
765
+ members.forEach((id, j) => {
766
+ // Keep multi-row columns aligned so vertical edge lanes never
767
+ // run through a centered, partially filled last row.
768
+ const count = Math.min(3, members.length),
769
+ x =
770
+ (WIDTH - (count * W + (count - 1) * G)) / 2 + (j % 3) * (W + G);
771
+ positions[id] = {
772
+ x,
773
+ y: y + Math.floor(j / 3) * (H + G),
774
+ w: W,
775
+ h: H,
776
+ rows,
777
+ bottom: y + height - 25,
778
+ };
779
+ });
780
+ y += height + 34;
781
+ }
782
+ size = { width: WIDTH, height: Math.max(240, y + 10) };
783
+ graph.setAttribute("viewBox", `0 0 ${size.width} ${size.height}`);
784
+ const defs = svg("defs"),
785
+ marker = svg("marker", {
786
+ id: "arrow",
787
+ viewBox: "0 0 10 10",
788
+ refX: 9,
789
+ refY: 5,
790
+ markerWidth: 6,
791
+ markerHeight: 6,
792
+ orient: "auto-start-reverse",
793
+ });
794
+ marker.append(
795
+ svg("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "#8197a9" }),
796
+ );
797
+ defs.append(marker);
798
+ graph.append(defs);
799
+ const edges = visibleEdges();
800
+ edges.forEach((e, index) => {
801
+ const a = positions[e.source],
802
+ b = positions[e.target];
803
+ let d;
804
+ if (e.source === e.target) {
805
+ d = `M ${a.x + a.w - 22} ${a.y} C ${a.x + a.w + 58} ${a.y - 54},${a.x + a.w + 58} ${a.y + a.h + 54},${a.x + a.w - 3} ${a.y + a.h - 15}`;
806
+ } else if (e.carried || b.y <= a.y) {
807
+ const rail = WIDTH - 18 - (index % 5) * 7;
808
+ d = `M ${a.x + a.w} ${a.y + 50} C ${rail} ${a.y + 50},${rail} ${b.y + 50},${b.x + b.w} ${b.y + 50}`;
809
+ } else if (a.rows > 1 || b.rows > 1) {
810
+ const out = a.x + a.w + 10,
811
+ into = b.x - 12,
812
+ middle = a.bottom + 12;
813
+ d = `M ${a.x + a.w} ${a.y + 50} H ${out} V ${middle} H ${into} V ${b.y + 50} H ${b.x}`;
814
+ } else {
815
+ const startX = a.x + a.w / 2,
816
+ endX = b.x + b.w / 2,
817
+ mid = (a.y + a.h + b.y) / 2;
818
+ d = `M ${startX} ${a.y + a.h} C ${startX} ${mid},${endX} ${mid},${endX} ${b.y}`;
819
+ }
820
+ const stroke = e.carried
821
+ ? "#8565b0"
822
+ : e.kinds.includes("failure_order")
823
+ ? "#c87373"
824
+ : e.kinds.includes("constraint")
825
+ ? "#c58a39"
826
+ : "#7294b4";
827
+ const line = svg("path", {
828
+ d,
829
+ stroke,
830
+ class: "edge" + (e.carried ? " carried" : ""),
831
+ "marker-end": "url(#arrow)",
832
+ });
833
+ line.dataset.source = e.source;
834
+ line.dataset.target = e.target;
835
+ graph.append(line);
836
+ const hit = svg("path", { d, class: "edge-hit" });
837
+ hit.append(
838
+ svg(
839
+ "title",
840
+ {},
841
+ `${e.source} → ${e.target}: ${e.kinds.join(" / ")}${e.carried ? " (loop-carried)" : ""}`,
842
+ ),
843
+ );
844
+ hit.onclick = () => {
845
+ selected = [e.source, e.target];
846
+ highlight();
847
+ showEdge(e);
848
+ };
849
+ graph.append(hit);
850
+ });
851
+ for (const n of report.nodes) {
852
+ const pos = positions[n.id],
853
+ g = svg("g", {
854
+ id: "node-" + n.id,
855
+ transform: `translate(${pos.x} ${pos.y})`,
856
+ class: "node",
857
+ tabindex: 0,
858
+ role: "button",
859
+ "aria-label": `${n.id} ${n.operation} ${n.label}`,
860
+ });
861
+ g.append(svg("title", {}, `${n.id} ${n.operation}\n${n.prompt}`));
862
+ g.append(
863
+ svg("rect", {
864
+ width: pos.w,
865
+ height: pos.h,
866
+ rx: 9,
867
+ fill: "#fff",
868
+ stroke: "#cdd9e2",
869
+ }),
870
+ );
871
+ g.append(
872
+ svg(
873
+ "text",
874
+ {
875
+ x: 12,
876
+ y: 21,
877
+ class: "id",
878
+ fill: color[n.operation] || "#486375",
879
+ },
880
+ `${n.id} ${n.operation}`,
881
+ ),
882
+ );
883
+ const chars = Array.from(n.label);
884
+ for (let row = 0; row < 2; row++) {
885
+ let text = chars.slice(row * 17, (row + 1) * 17).join("");
886
+ if (row === 1 && chars.length > 34) text += "…";
887
+ g.append(
888
+ svg("text", { x: 12, y: 43 + row * 17, class: "label" }, text),
889
+ );
890
+ }
891
+ g.append(
892
+ svg(
893
+ "text",
894
+ { x: 12, y: 85, class: "meta" },
895
+ [n.request, n.loops.join("/"), n.gates.length ? "条件激活" : ""]
896
+ .filter(Boolean)
897
+ .join(" · "),
898
+ ),
899
+ );
900
+ g.onclick = () => select([n.id]);
901
+ g.onkeydown = (e) => {
902
+ if (e.key === "Enter" || e.key === " ") {
903
+ e.preventDefault();
904
+ select([n.id]);
905
+ }
906
+ };
907
+ graph.append(g);
908
+ }
909
+ if (!report.nodes.length) {
910
+ graph.append(
911
+ svg(
912
+ "text",
913
+ { x: 200, y: 100, fill: "#617286" },
914
+ "该 Plan 没有 Infer 调用。",
915
+ ),
916
+ );
917
+ }
918
+ $("status").textContent =
919
+ `${edges.length} / ${report.edges.length} 条投影依赖`;
920
+ zoom();
921
+ highlight();
922
+ }
923
+ $("mode").onchange = () => {
924
+ mode = $("mode").value;
925
+ report = DATA[mode];
926
+ selected = [];
927
+ draw();
928
+ fit();
929
+ opportunities();
930
+ $("canvas").scrollTop = 0;
931
+ };
932
+ $("transitive").onchange = draw;
933
+ $("carried").onchange = draw;
934
+ $("search").oninput = highlight;
935
+ $("plus").onclick = () => {
936
+ scale = Math.min(2, scale + 0.15);
937
+ zoom();
938
+ };
939
+ $("minus").onclick = () => {
940
+ scale = Math.max(0.2, scale - 0.15);
941
+ zoom();
942
+ };
943
+ $("fit").onclick = fit;
944
+ $("reset").onclick = () => {
945
+ $("search").value = "";
946
+ select([]);
947
+ };
948
+ draw();
949
+ fit();
950
+ opportunities();
951
+ </script>
952
+ </body>
953
+ </html>