hypgraph 0.7.4__tar.gz → 0.7.6__tar.gz

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 (97) hide show
  1. {hypgraph-0.7.4 → hypgraph-0.7.6}/CHANGELOG.md +29 -0
  2. {hypgraph-0.7.4 → hypgraph-0.7.6}/PKG-INFO +1 -1
  3. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/__init__.py +1 -1
  4. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/derive.py +11 -6
  5. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/hypgraph.css +41 -5
  6. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/hypgraph.js +73 -11
  7. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/index.html +1 -1
  8. hypgraph-0.7.6/tests/test_cited_and_failed_support.py +60 -0
  9. {hypgraph-0.7.4 → hypgraph-0.7.6}/.gitignore +0 -0
  10. {hypgraph-0.7.4 → hypgraph-0.7.6}/AGENTS.md +0 -0
  11. {hypgraph-0.7.4 → hypgraph-0.7.6}/CITATION.cff +0 -0
  12. {hypgraph-0.7.4 → hypgraph-0.7.6}/LICENSE +0 -0
  13. {hypgraph-0.7.4 → hypgraph-0.7.6}/NOTICE +0 -0
  14. {hypgraph-0.7.4 → hypgraph-0.7.6}/README.md +0 -0
  15. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/PLAN.md +0 -0
  16. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
  17. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/decisions/ADR-002-governance-profiles.md +0 -0
  18. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/profile-workflows-plan.md +0 -0
  19. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/review-2026-10-24.md +0 -0
  20. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/site-plan.md +0 -0
  21. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/spec.md +0 -0
  22. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/ui-plan.md +0 -0
  23. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/workflows/README.md +0 -0
  24. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/workflows/knowledge.md +0 -0
  25. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/workflows/optimization.md +0 -0
  26. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/workflows/scientific.md +0 -0
  27. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/workflows/software.md +0 -0
  28. {hypgraph-0.7.4 → hypgraph-0.7.6}/docs/workflows/trading.md +0 -0
  29. {hypgraph-0.7.4 → hypgraph-0.7.6}/pyproject.toml +0 -0
  30. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/__main__.py +0 -0
  31. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/bundle.py +0 -0
  32. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/checks.py +0 -0
  33. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/cli.py +0 -0
  34. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/coord.py +0 -0
  35. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/demos/__init__.py +0 -0
  36. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/demos/broad_street.py +0 -0
  37. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/graph.py +0 -0
  38. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/language.py +0 -0
  39. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/mcp.py +0 -0
  40. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/plan.py +0 -0
  41. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/policy.py +0 -0
  42. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/profiles.py +0 -0
  43. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/refusals.py +0 -0
  44. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/report_check.py +0 -0
  45. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/schema.py +0 -0
  46. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/server.py +0 -0
  47. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/skill/SKILL.md +0 -0
  48. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/stats.py +0 -0
  49. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/store.py +0 -0
  50. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/__init__.py +0 -0
  51. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/atlas.css +0 -0
  52. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/atlas.js +0 -0
  53. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/hypgraph.svg +0 -0
  54. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
  55. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/vendor/cose-base.js +0 -0
  56. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
  57. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
  58. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/ui/vendor/layout-base.js +0 -0
  59. {hypgraph-0.7.4 → hypgraph-0.7.6}/src/hypgraph/workflow.py +0 -0
  60. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_accept_work_kind.py +0 -0
  61. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_bound_support.py +0 -0
  62. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_bundle_telemetry.py +0 -0
  63. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_cancel_releases_lease.py +0 -0
  64. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_coord.py +0 -0
  65. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_d10.py +0 -0
  66. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_engine.py +0 -0
  67. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_engine_fixes.py +0 -0
  68. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_incremental.py +0 -0
  69. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_link_translate.py +0 -0
  70. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_log_and_check.py +0 -0
  71. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_measure_questions.py +0 -0
  72. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_opposed_answers.py +0 -0
  73. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_option_lead.py +0 -0
  74. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_order_detail.py +0 -0
  75. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_profiles.py +0 -0
  76. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_profiles_041.py +0 -0
  77. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_questions_not_assertions.py +0 -0
  78. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_refusal_friction.py +0 -0
  79. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_refusal_results.py +0 -0
  80. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_refusals.py +0 -0
  81. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_relation_words.py +0 -0
  82. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_report_answers.py +0 -0
  83. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_rerunnable.py +0 -0
  84. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_restates_scale.py +0 -0
  85. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_server_auth.py +0 -0
  86. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_shape_refusals.py +0 -0
  87. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_stats_not_backtests.py +0 -0
  88. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_stats_values.py +0 -0
  89. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_surfaces.py +0 -0
  90. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_telemetry_stream.py +0 -0
  91. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_ui_app.py +0 -0
  92. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_ui_engine.py +0 -0
  93. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_uncorrected_effect.py +0 -0
  94. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_unnamed_inputs.py +0 -0
  95. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_versions_not_figures.py +0 -0
  96. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_vocabulary_friction.py +0 -0
  97. {hypgraph-0.7.4 → hypgraph-0.7.6}/tests/test_workflows.py +0 -0
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.6 — 2026-10-29: the swarm as swimlanes, and the UI walked on a real record
4
+
5
+ - **UI polish from a walk of a real swarm record.** Identical alerts collapse into one line with a count (a seat
6
+ that looped four times showed four lines); the six KPI tiles fit one row where the sixth sat orphaned below;
7
+ the grids break on the panel's own width, not the window's (container queries: an embed is narrower than its
8
+ page, and the Live view squeezed its graph to 130 px between two full columns); the theme follows the OS unless
9
+ the visitor chose one; a refusal relayed twice ("claim refused: claim
10
+ refused:") reads once in the event stream; `mount(…, {embed: true})` marks an app on a page of its own: it
11
+ grows to its content and scrolls nothing inside, so a wheel over it moves the page (twenty ticks over the
12
+ homepage's embed used to move nothing), and its feed shows the latest 15 events. hypgraph.com: the hero's chips no
13
+ longer wrap mid-phrase.
14
+
15
+ - **The Live view shows the swarm as swimlanes.** Seats as rows in the order they first took work, time across,
16
+ every work order a bar coloured by how the record settled it (accepted, reworked, returned, cancelled, in
17
+ flight), every model call a tick above the bar and every tool call a tick below, refusals and failures in
18
+ red, the scheduler's reworks and escalations as marks on their own row, and the playhead sweeping across
19
+ during replay on a fixed axis. Hover anything for its figures; click an order for its trace. Drawn from the
20
+ telemetry the view already holds, no dependency.
21
+
22
+ ## 0.7.5 — 2026-10-28: a cited source is a line of support, and a finding of failure is not a fall
23
+
24
+ - **A cited source is a line of support, and a succeeded run's "failed" outcome is a finding, not a fall.**
25
+ A knowledge claim that cites its sources (`cites` → source, or `derived_from` → source) read "no live
26
+ support · unverified" even after 0.7.4 counted bound results; the source is what such a claim rests on,
27
+ and its line is `external`. And a result with `outcome: failed` counted as a broken premise whatever its
28
+ run did: testers write "failed" for the code under test failing its spec (30 results in the challenge
29
+ records, 13 carrying checked claims that then read UNSUPPORTED — every support has fallen). A result a
30
+ succeeded run produced did happen; it falls only when there is no run or the run did not succeed.
31
+
3
32
  ## 0.7.4 — 2026-10-28: a bound result is a line of support, and a relation word is read as the relation it names
4
33
 
5
34
  - **A bound result is a line of support.** A claim's standing counted only `supports` edges into it, so a claim
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hypgraph
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: The evidence layer for AI research agents: a hash-chained research record that checks every claim as it is written and derives how well each is known.
5
5
  Project-URL: Homepage, https://hypgraph.com
6
6
  Project-URL: Documentation, https://hypgraph.com/engineers/
@@ -16,7 +16,7 @@ Standing is derived on every read, never stored; every write is an event in a SH
16
16
  their numbers; reviews pin versions; independence is counted, not assumed. See https://hypgraph.com.
17
17
  """
18
18
 
19
- __version__ = "0.7.4"
19
+ __version__ = "0.7.6"
20
20
 
21
21
  from .bundle import verify_bundle # noqa: E402
22
22
  from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
@@ -134,7 +134,11 @@ def fallen(s: Snap, nid: str) -> bool:
134
134
  if n["type"] == "run" and n["body"].get("state") in ("failed", "timeout", "cancelled", "interrupted"):
135
135
  return True
136
136
  if n["type"] == "result" and n["body"].get("outcome") == "failed":
137
- return True
137
+ # "failed" means the measurement didn't happen. A result a succeeded run produced did happen: its author
138
+ # wrote "failed" for what it measured (the code under test failing its spec — 30 results in the challenge
139
+ # records, 13 carrying checked claims that then read UNSUPPORTED). That is a finding, not a fall.
140
+ run = producing_run(s, nid)
141
+ return not (run and s.nodes[run]["body"].get("state") == "succeeded")
138
142
  return False
139
143
 
140
144
 
@@ -349,8 +353,8 @@ def dependents(s: Snap, nid: str, limit: int = 500) -> list[tuple[str, str, int]
349
353
 
350
354
 
351
355
  def supporters(s: Snap, nid: str) -> list[dict]:
352
- """What stands behind a node: every `supports` edge into it, and — for a claim — the results it is derived from
353
- and the results its measurements bind (each once). A claim that binds its numbers to a result rests on that
356
+ """What stands behind a node: every `supports` edge into it, and — for a claim — the results it is derived from,
357
+ the sources it derives from or cites, and the results its measurements bind (each once). A claim that binds its numbers to a result rests on that
354
358
  result whether or not anyone also wrote `supports`; until 0.7.4 only the edge counted, and most honest claims
355
359
  read "no live support" with their evidence "unverified"."""
356
360
  out = [e for e in s.inn.get(nid, []) if e["rel"] == "supports"]
@@ -358,10 +362,11 @@ def supporters(s: Snap, nid: str) -> list[dict]:
358
362
  if n and n["type"] == "claim":
359
363
  seen = {e["src"] for e in out}
360
364
  for e in s.out.get(nid, []):
361
- if e["rel"] == "derived_from" and e["dst"] in s.nodes and s.nodes[e["dst"]]["type"] == "result" \
362
- and e["dst"] not in seen:
365
+ # a result it derives from, or a source it derives from or cites: what the claim rests on
366
+ if e["rel"] in ("derived_from", "cites") and e["dst"] in s.nodes and e["dst"] not in seen \
367
+ and s.nodes[e["dst"]]["type"] in (("result", "source") if e["rel"] == "derived_from" else ("source",)):
363
368
  seen.add(e["dst"])
364
- out.append({"src": e["dst"], "rel": "derived_from", "dst": nid, "data": {}, "by": n["created_by"]})
369
+ out.append({"src": e["dst"], "rel": e["rel"], "dst": nid, "data": {}, "by": n["created_by"]})
365
370
  for m in n["body"].get("measurements") or []:
366
371
  rid = m.get("result") if isinstance(m, dict) else None
367
372
  if rid and rid in s.nodes and s.nodes[rid]["type"] == "result" and rid not in seen:
@@ -64,7 +64,7 @@
64
64
  .hg .inv { display: grid; grid-template-columns: 8px 1fr; gap: 9px; align-items: start; }
65
65
  .hg .inv .dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; }
66
66
  .hg .inv .t { font-weight: 550; color: var(--ink); line-height: 1.3; } .hg .inv .s { font-size: 11.5px; color: var(--ink3); }
67
- .hg .hg-main { overflow: auto; padding: 22px 26px 40px; min-height: 0; }
67
+ .hg .hg-main { overflow: auto; padding: 22px 26px 40px; min-height: 0; container: hg-main / inline-size; }
68
68
  .hg .head { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
69
69
  .hg .head h1 { margin: 0; font-size: 20px; letter-spacing: -.015em; }
70
70
  .hg .head p { margin: 4px 0 0; color: var(--ink2); max-width: 820px; }
@@ -84,13 +84,21 @@
84
84
  .hg .card > h3 .r { margin-left: auto; display: flex; gap: 6px; }
85
85
  .hg .card .body { padding: 12px 14px; }
86
86
  .hg .grid { display: grid; gap: 14px; }
87
- .hg .kpis { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
87
+ .hg .kpis { grid-template-columns: repeat(6, minmax(0, 1fr)); } /* 6, 3 or 2 to a row: never an orphan */
88
+ @container hg-main (max-width: 900px) { .hg .kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
89
+ @container hg-main (max-width: 460px) { .hg .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
90
+ /* embedded on a page of its own: the window grows to its content and scrolls nothing inside, so a wheel over it
91
+ moves the page; the feed shows the latest events instead of scrolling */
92
+ .hg.hg-embed .hg-app { height: auto; grid-template-rows: 52px auto; }
93
+ .hg.hg-embed .hg-main, .hg.hg-embed .hg-nav { overflow: visible; }
94
+ .hg.hg-embed .feed { max-height: none; overflow: visible; }
95
+ .hg.hg-embed { overflow: visible; }
88
96
  .hg .kpi { padding: 12px 14px; } .hg .kpi .k { color: var(--ink2); font-size: 12px; }
89
97
  .hg .kpi .v { font-size: 22px; font-weight: 650; letter-spacing: -.02em; margin-top: 2px; font-variant-numeric: tabular-nums; }
90
98
  .hg .kpi .s { color: var(--ink3); font-size: 11.5px; margin-top: 2px; }
91
99
  .hg .kpi.warn .v { color: var(--warn); } .hg .kpi.bad .v { color: var(--bad); } .hg .kpi.ok .v { color: var(--ok); }
92
100
  .hg .two { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
93
- @media (max-width: 1100px) { .hg .two { grid-template-columns: 1fr; } }
101
+ @container hg-main (max-width: 860px) { .hg .two { grid-template-columns: 1fr; } }
94
102
  .hg table { width: 100%; border-collapse: collapse; }
95
103
  .hg th, .hg td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
96
104
  .hg th { font-size: 11.5px; font-weight: 600; color: var(--ink3); text-transform: uppercase; letter-spacing: .04em;
@@ -146,7 +154,7 @@
146
154
  @keyframes hg-pulse { 0% { box-shadow: 0 0 0 0 rgba(18, 183, 106, .55); } 70% { box-shadow: 0 0 0 9px rgba(18, 183, 106, 0); } 100% { box-shadow: 0 0 0 0 rgba(18, 183, 106, 0); } }
147
155
  .hg .scrub { flex: 1; min-width: 160px; accent-color: var(--accent); }
148
156
  .hg .live { grid-template-columns: minmax(240px, 300px) minmax(0, 1fr) minmax(280px, 380px); }
149
- @media (max-width: 1250px) { .hg .live { grid-template-columns: 1fr; } }
157
+ @container hg-main (max-width: 1000px) { .hg .live { grid-template-columns: 1fr; } } /* the panel's width, not the window's: an embed is narrower than its page */
150
158
  .hg .seat { display: grid; grid-template-columns: 30px 1fr auto; gap: 10px; align-items: center; padding: 9px 14px; border-bottom: 1px solid var(--line); }
151
159
  .hg .seat .av { width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; font-weight: 700; font-size: 12px;
152
160
  background: var(--sunken); color: var(--ink2); }
@@ -160,6 +168,34 @@
160
168
  .hg .tel-bad { color: var(--bad); font-weight: 600; } .hg .tel-warn { color: var(--warn); font-weight: 600; }
161
169
  .hg .fly { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); margin-right: 6px;
162
170
  vertical-align: 0; animation: hg-pulse 1.2s infinite; }
171
+ /* the timeline: seats as rows, orders as bars, calls as ticks above (model) and below (tool) the bar */
172
+ .hg .lanes { display: grid; gap: 3px; }
173
+ .hg .lane-row { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 10px; align-items: center; }
174
+ .hg .lane-lbl { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; font-size: 12px; }
175
+ .hg .lane-lbl b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hg .lane-lbl .dim { font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
176
+ .hg .lane { position: relative; height: 28px; background: var(--sunken); border-radius: 4px; }
177
+ .hg .lane .ord { position: absolute; top: 8px; height: 12px; border: 0; border-radius: 3px; padding: 0 4px; margin: 0; font: 600 10px/12px var(--mono);
178
+ color: #fff; text-align: left; overflow: hidden; white-space: nowrap; cursor: pointer; opacity: .92; box-sizing: border-box; }
179
+ .hg .lane .ord:hover, .hg .lane .ord:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; z-index: 3; }
180
+ .hg .ord.ok { background: var(--ok); } .hg .ord.warn { background: var(--warn); } .hg .ord.info { background: var(--info); }
181
+ .hg .ord.off { background: var(--ink3); } .hg .ord.done { background: var(--accent-ink); } .hg .ord.open { background: var(--accent); animation: hg-blink 1.2s ease-in-out infinite; }
182
+ .hg .lane .tick { position: absolute; top: 2px; height: 5px; min-width: 2px; border-radius: 1px; background: var(--ink); opacity: .6; z-index: 1; }
183
+ .hg .lane .tick.tool { top: 21px; background: var(--ink3); }
184
+ .hg .lane .tick.refused, .hg .lane .tick.bad { top: 21px; height: 6px; background: var(--bad); opacity: 1; min-width: 3px; z-index: 2; }
185
+ .hg .lane .tick.model.bad { top: 1px; }
186
+ .hg .lane .tick.open { opacity: 1; background: var(--accent); animation: hg-blink 1.2s ease-in-out infinite; }
187
+ .hg .lane .mark { position: absolute; top: 9px; width: 10px; height: 10px; margin-left: -5px; background: var(--warn); transform: rotate(45deg); border-radius: 2px; z-index: 1; }
188
+ .hg .lane .playhead { position: absolute; top: -2px; bottom: -2px; width: 2px; margin-left: -1px; background: var(--bad); z-index: 4; pointer-events: none; }
189
+ .hg .lane-row.axis .lane { height: 16px; background: none; }
190
+ .hg .lane-row.axis .lane span { position: absolute; top: 1px; font: 10.5px var(--mono); color: var(--ink3); transform: translateX(-50%); }
191
+ .hg .lane-row.axis .lane span:first-child { transform: none; } .hg .lane-row.axis .lane span:last-child { transform: translateX(-100%); }
192
+ .hg .legend { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; font-size: 11.5px; margin-top: 10px; }
193
+ .hg .legend .ord, .hg .legend .tick, .hg .legend .mark { position: static; display: inline-block; width: 16px; height: 10px; margin: 0 2px 0 0; vertical-align: middle; animation: none; }
194
+ .hg .legend .tick { height: 5px; } .hg .legend .mark { width: 8px; height: 8px; transform: rotate(45deg); } .hg .legend .gap { width: 10px; }
195
+ @container hg-main (max-width: 560px) { .hg .lane-row { grid-template-columns: 84px minmax(0, 1fr); } .hg .lane-lbl .dim { display: none; }
196
+ .hg .lane-row.axis .lane span:nth-child(even) { display: none; } }
197
+ @keyframes hg-blink { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
198
+ @media (prefers-reduced-motion: reduce) { .hg .ord.open, .hg .lane .tick.open { animation: none; } }
163
199
  .hg .trace { display: grid; gap: 4px; }
164
200
  .hg .trace .tr { display: grid; grid-template-columns: minmax(160px, 300px) 1fr 64px; gap: 10px; align-items: center; font-size: 12px; }
165
201
  .hg .trace .tl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink2); }
@@ -171,7 +207,7 @@
171
207
  .hg .alerts li { display: flex; gap: 8px; align-items: baseline; padding: 6px 0; }
172
208
  .hg .alerts { list-style: none; margin: 0; padding: 0; }
173
209
  .hg .kanban { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
174
- @media (max-width: 1000px) { .hg .kanban { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
210
+ @container hg-main (max-width: 780px) { .hg .kanban { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
175
211
  .hg .col { background: var(--surface2); border: 1px solid var(--line); border-radius: 9px; padding: 8px; min-height: 120px; }
176
212
  .hg .col h3 { margin: 2px 4px 8px; font-size: 11.5px; color: var(--ink3); text-transform: uppercase; letter-spacing: .05em; display: flex; }
177
213
  .hg .col h3 b { margin-left: auto; color: var(--ink2); }
@@ -10,13 +10,15 @@
10
10
  * // or, with no server at all: { source: HypGraph.BundleSource(bundleJson) }
11
11
  * </script>
12
12
  *
13
- * Views (docs/ui-plan.md): Overview · Live · Pipeline · Graph · Evidence · Work orders · Audit log · Report, each in
13
+ * Views (docs/ui-plan.md): Overview · Live (with the swarm as swimlanes) · Pipeline · Graph · Evidence · Work orders · Audit log · Report, each in
14
14
  * the investigation's profile's own words (Requirement, Thesis, Solution…). Nothing here writes. Standing, stages and
15
15
  * gates come from the engine; a BundleSource shows what the bundle was exported with and re-verifies its hash chain
16
16
  * in the browser. An HttpSource follows the record live (GET /stream), falling back to polling on older hosts, and
17
17
  * hides what a host's API doesn't serve.
18
18
  *
19
- * Options: source (required) · theme ('light' | 'dark') · hash (keep the view in location.hash) · title · live ·
19
+ * Options: source (required) · theme ('light' | 'dark'; the OS's choice by default) · embed (on a page of its own:
20
+ * it grows to its content and scrolls nothing inside, the feed shows the latest 15 events) · hash (keep the view in
21
+ * location.hash) · title · live ·
20
22
  * play (open on the Live view and replay the run from its first event; also `#view=live&play=1`, and skipped when
21
23
  * the visitor asks for reduced motion) · onTheme(t) · operatorUrl · fileUrl(path). Keys: 1–8 views · ⌘K / Ctrl+K or / search · Esc close · t theme.
22
24
  * `HypAtlas` is kept as an alias of `HypGraph` for one release.
@@ -321,7 +323,9 @@
321
323
  opts = opts || {};
322
324
  const src = opts.source || HttpSource();
323
325
  root.classList.add('hg');
324
- if (!root.dataset.theme) root.dataset.theme = opts.theme || 'light';
326
+ const prefersDark = () => !!(global.matchMedia && global.matchMedia('(prefers-color-scheme: dark)').matches);
327
+ if (!root.dataset.theme) root.dataset.theme = opts.theme || (prefersDark() ? 'dark' : 'light');
328
+ if (opts.embed) root.classList.add('hg-embed'); // on a page of its own, not the workbench: short feeds, no scroll trap
325
329
  const brand = opts.title && !/^atlas$/i.test(opts.title) ? esc(opts.title) : 'Hyp<b>Graph</b>';
326
330
  root.innerHTML = `<div class="hg-app">
327
331
  <header class="hg-top"><div class="hg-brand">${LOGO}<span>${brand}</span></div>
@@ -454,9 +458,14 @@
454
458
  if (m.tel.length) out.push(...telAlerts(m.tel));
455
459
  return out;
456
460
  }
457
- const alertList = (items) => items.length ? `<ul class="alerts">${items.map(([lvl, txt, code]) =>
458
- `<li><span class="pill ${lvl}">${lvl === 'bad' ? 'action' : lvl === 'warn' ? 'check' : 'note'}</span><span>${esc(txt)}${code ? ` <button class="btn" type="button" data-n="${esc(code)}" style="padding:1px 8px">open</button>` : ''}</span></li>`).join('')}</ul>`
459
- : '<div class="dim">Nothing needs attention.</div>';
461
+ const alertList = (items) => {
462
+ const seen = new Map(); // the same alert raised four times is one line, ×4
463
+ items.forEach(([lvl, txt, code]) => { const k = lvl + '|' + txt, h = seen.get(k); if (h) h.n++; else seen.set(k, { lvl, txt, code, n: 1 }); });
464
+ const rows = [...seen.values()];
465
+ return rows.length ? `<ul class="alerts">${rows.map(({ lvl, txt, code, n }) =>
466
+ `<li><span class="pill ${lvl}">${lvl === 'bad' ? 'action' : lvl === 'warn' ? 'check' : 'note'}</span><span>${esc(txt)}${n > 1 ? ` <span class="pill neutral" title="raised ${n} times">×${n}</span>` : ''}${code ? ` <button class="btn" type="button" data-n="${esc(code)}" style="padding:1px 8px">open</button>` : ''}</span></li>`).join('')}</ul>`
467
+ : '<div class="dim">Nothing needs attention.</div>';
468
+ };
460
469
 
461
470
  // ── telemetry (what the swarm did that the record can't see) ──
462
471
  function telUpTo() { // the telemetry the current replay position has reached
@@ -515,7 +524,7 @@
515
524
  switch (t.kind) {
516
525
  case 'model_end': what = t.ok === false ? `<span class="tel-bad">model call failed</span> ${d(String(t.error || '').slice(0, 120))}`
517
526
  : `model ${d(t.model || '')} · ${fmtN(t.in || 0)}→${fmtN(t.out || 0)} tokens${t.usd ? ' · ' + fmtUsd(t.usd) : ''} · ${t.s}s${(t.tool_calls || []).length ? ' → ' + t.tool_calls.map(esc).join(', ') : ''}`; break;
518
- case 'tool_end': what = refusedCall(t) ? `<span class="tel-warn">${esc(t.tool)} refused by the record</span> ${d(String(t.error || '').replace(/^refused:?\s*/, '').slice(0, 140))}`
527
+ case 'tool_end': what = refusedCall(t) ? `<span class="tel-warn">${esc(t.tool)} refused by the record</span> ${d(String(t.error || '').replace(/^refused:?\s*/, '').replace(/\b(\w+ refused):\s*\1:\s*/g, '$1: ').slice(0, 140))}`
519
528
  : t.ok === false ? `<span class="tel-bad">tool ${esc(t.tool)} failed</span> ${d(String(t.error || '').slice(0, 120))}` : `tool ${c(t.tool)} · ${t.s}s`; break;
520
529
  case 'order_start': what = `took ${c(t.order)} ${d('· ' + (t.model || ''))}`; break;
521
530
  case 'order_end': what = `finished ${c(t.order)} · ${t.calls || 0} call(s)${t.stopped ? ' ' + d('· ' + t.stopped) : ''}`; break;
@@ -552,6 +561,57 @@
552
561
  <div class="trace">${rows || '<div class="dim">No calls yet.</div>'}</div>`;
553
562
  }
554
563
 
564
+ function lanes(tel, running) { // the swarm as swimlanes: seats as rows, orders as bars, calls as ticks
565
+ const m = S.m, all = m.tel, following = S.cursor == null;
566
+ if (!all.length) return '';
567
+ const run = all.find((t) => t.kind === 'run_start'), end = all.find((t) => t.kind === 'run_end');
568
+ const seen = tel.length ? tel[tel.length - 1].ts : (run ? run.ts : all[0].ts);
569
+ const now = following && running ? Date.now() / 1000 : seen; // the replay position, or the wall clock
570
+ const t0 = run ? run.ts : all[0].ts;
571
+ const t1 = Math.max(end ? end.ts : Math.max(all[all.length - 1].ts, now), t0 + 1); // a fixed axis while replaying
572
+ const span = t1 - t0, x = (ts) => Math.min(100, Math.max(0, ((ts - t0) / span) * 100));
573
+ const fmtT = (s) => s >= 3600 ? `${Math.floor(s / 3600)}h${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}` : `${Math.floor(s / 60)}:${String(Math.floor(Math.max(s, 0) % 60)).padStart(2, '0')}`;
574
+ const starts = tel.filter((t) => t.kind === 'order_start');
575
+ if (!starts.length) return '<div class="empty">Nobody has started yet.</div>';
576
+ const status = new Map((m.orders || []).map((o) => [o.code, o.status]));
577
+ const ends = new Map(tel.filter((t) => t.kind === 'order_end').map((t) => [t.span, t]));
578
+ const startOf = new Map(tel.filter((t) => t.kind === 'model_start' || t.kind === 'tool_start').map((t) => [t.span, t]));
579
+ const calls = tel.filter((t) => t.kind === 'model_end' || t.kind === 'tool_end');
580
+ const open = openSpans(tel).filter((t) => t.kind === 'model_start' || t.kind === 'tool_start');
581
+ const seats = [...new Set(starts.map((o) => o.seat).concat(calls.map((t) => t.seat)).filter(Boolean))]; // in the order they first took work; a seat that only made calls (the lead's report) still gets its row
582
+ const head = (following && running) || S.playing || !following ? `<u class="playhead" style="left:${x(now).toFixed(2)}%" title="${esc(fmtT(now - t0))}"></u>` : '';
583
+ const rows = seats.map((seat) => {
584
+ const bars = starts.filter((o) => o.seat === seat).map((o) => {
585
+ const e = ends.get(o.span), st = e ? (status.get(o.order) || 'ended') : 'open'; // until it ends, the viewer doesn't know how it ends
586
+ const a = x(o.ts), b = x(e ? e.ts : now);
587
+ const cls = st === 'accepted' ? 'ok' : st === 'reworked' ? 'warn' : st === 'cancelled' ? 'off' : st === 'returned' ? 'info' : e ? 'done' : 'open';
588
+ const title = `${o.order} · ${seat}${o.model ? ' · ' + o.model : ''}\n${e ? `${Number(e.s || e.ts - o.ts).toFixed(1)}s · ${e.calls || 0} call(s)${e.stopped ? ' · ' + e.stopped : ''}` : 'in flight'}\n${st}`;
589
+ return `<button type="button" class="ord ${cls}" data-act="lv-trace" data-order="${esc(o.order)}" style="left:${a.toFixed(2)}%;width:${Math.max(b - a, 0.4).toFixed(2)}%" title="${esc(title)}">${esc(o.order)}</button>`;
590
+ }).join('');
591
+ const ticks = calls.filter((t) => t.seat === seat).map((t) => {
592
+ const s = startOf.get(t.span), ts = s ? s.ts : t.ts - (Number(t.s) || 0);
593
+ const a = x(ts), w = Math.max(((Number(t.s) || 0) / span) * 100, 0.15);
594
+ const kind = t.kind === 'model_end' ? (t.ok === false ? 'bad' : 'model') : refusedCall(t) ? 'refused' : failed(t) ? 'bad' : 'tool';
595
+ const title = t.kind === 'model_end' ? `model ${t.model || ''} · ${fmtN(t.in || 0)}→${fmtN(t.out || 0)} tokens${t.usd ? ' · ' + fmtUsd(t.usd) : ''} · ${t.s}s${t.ok === false ? '\n' + String(t.error || '').slice(0, 120) : ''}`
596
+ : `${t.tool} · ${t.s}s${t.ok === false ? '\n' + String(t.error || '').replace(/^refused:?\s*/, '').slice(0, 120) : ''}`;
597
+ return `<u class="tick ${kind}" style="left:${a.toFixed(2)}%;width:${Math.min(w, 100 - a).toFixed(2)}%" title="${esc(title)}"></u>`;
598
+ }).join('');
599
+ const flying = open.filter((t) => t.seat === seat).map((t) => `<u class="tick open ${t.kind === 'model_start' ? 'model' : 'tool'}" style="left:${x(t.ts).toFixed(2)}%;width:${Math.max(x(now) - x(t.ts), 0.15).toFixed(2)}%" title="${esc(t.kind === 'model_start' ? `calling ${t.model || 'the model'}` : `running ${t.tool}`)}"></u>`).join('');
600
+ const mine = calls.filter((t) => t.seat === seat), n = mine.filter((t) => t.kind === 'model_end').length;
601
+ const usd = mine.reduce((a, t) => a + (Number(t.usd) || 0), 0), refs = mine.filter(refusedCall).length;
602
+ return `<div class="lane-row"><div class="lane-lbl"><b>${esc(seat)}</b><span class="dim">${n} call${n === 1 ? '' : 's'}${usd ? ' · ' + fmtUsd(usd) : ''}${refs ? ` · <span class="tel-warn">${refs} refused</span>` : ''}</span></div><div class="lane">${bars}${ticks}${flying}${head}</div></div>`;
603
+ }).join('');
604
+ const marks = tel.filter((t) => t.kind === 'decision' && /rework|escalat|cancel|moot/.test(String(t.event || ''))).map((t) =>
605
+ `<u class="mark" style="left:${x(t.ts).toFixed(2)}%" title="${esc(`scheduler: ${t.event}${t.order ? ' ' + t.order : ''}${t.why || t.missing ? ' — ' + String(t.why || t.missing).slice(0, 100) : ''}`)}"></u>`).join('');
606
+ const sched = marks ? `<div class="lane-row"><div class="lane-lbl"><b>scheduler</b><span class="dim">reworks · escalations</span></div><div class="lane">${marks}${head}</div></div>` : '';
607
+ const step = [5, 10, 15, 30, 60, 120, 300, 600, 1200, 1800, 3600].find((s) => span / s <= 8) || 3600;
608
+ let ax = '';
609
+ for (let s = 0; s <= span; s += step) ax += `<span style="left:${((s / span) * 100).toFixed(2)}%">${fmtT(s)}</span>`;
610
+ return `<div class="lanes">${rows}${sched}<div class="lane-row axis"><div class="lane-lbl"></div><div class="lane">${ax}</div></div></div>
611
+ <div class="legend dim"><i class="ord ok"></i> accepted <i class="ord warn"></i> reworked <i class="ord info"></i> returned <i class="ord off"></i> cancelled <i class="ord open"></i> in flight
612
+ <span class="gap"></span><u class="tick model"></u> model call <u class="tick tool"></u> tool call <u class="tick refused"></u> refused by the record <u class="tick bad"></u> failed <u class="mark"></u> rework · escalation</div>`;
613
+ }
614
+
555
615
  // ── views ──
556
616
  function vOverview() {
557
617
  const m = S.m, qs = m.board || [], claims = m.nodes.filter((n) => n.type === 'claim');
@@ -714,6 +774,7 @@
714
774
  <button class="btn" type="button" data-act="lv-live" ${following ? 'disabled' : ''}>${live ? 'Follow live' : 'Jump to end'}</button></div></div>
715
775
  ${tk.length ? `<div class="grid kpis" style="margin-bottom:10px">${tk.map(([k, v, s, c]) => `<div class="card kpi ${c || ''}"><div class="k">${esc(k)}</div><div class="v">${v}</div>${s ? `<div class="s">${esc(s)}</div>` : ''}</div>`).join('')}</div>` : ''}
716
776
  <div class="grid kpis" style="margin-bottom:14px">${kp.map(([k, v]) => `<div class="card kpi"><div class="k">${esc(k)}</div><div class="v">${v}</div></div>`).join('')}</div>
777
+ ${m.tel.length ? `<div class="card" style="margin-bottom:14px"><h3>Timeline <span class="sub">who held what, when — every call a tick; click an order for its trace</span></h3><div class="body">${lanes(tel, running)}</div></div>` : ''}
717
778
  ${m.tel.length ? (() => { const a = telAlerts(tel); return a.length ? `<div class="card" style="margin-bottom:14px"><h3>Alerts</h3><div class="body">${alertList(a)}</div></div>` : ''; })() : ''}
718
779
  <div class="grid live">
719
780
  <div class="card"><h3>Seats <span class="sub">who is working on what</span></h3>${seats(seen, tel)}</div>
@@ -725,10 +786,10 @@
725
786
  ${m.tel.length ? `<div class="card" style="margin-top:14px"><h3>Trace <span class="sub">where a work order's time went: each model and tool call</span></h3><div class="body">${trace(tel)}</div></div>` : ''}`;
726
787
  }
727
788
  function feed(seen, tel, running) {
728
- const mode = S.feed || 'all';
729
- const rec = mode === 'telemetry' ? [] : seen.slice(-300).map((e) => ({ ts: Date.parse(e.at) / 1000, e }));
730
- const tl = mode === 'record' ? [] : tel.filter((t) => !t.kind.endsWith('_start') || t.kind === 'order_start' || t.kind === 'run_start').slice(-400).map((t) => ({ ts: t.ts, t }));
731
- const all = rec.concat(tl).sort((a, b) => b.ts - a.ts || ((b.e ? b.e.seq : 0) - (a.e ? a.e.seq : 0))).slice(0, 400);
789
+ const mode = S.feed || 'all', cap = opts.embed ? 15 : 400;
790
+ const rec = mode === 'telemetry' ? [] : seen.slice(-cap).map((e) => ({ ts: Date.parse(e.at) / 1000, e }));
791
+ const tl = mode === 'record' ? [] : tel.filter((t) => !t.kind.endsWith('_start') || t.kind === 'order_start' || t.kind === 'run_start').slice(-cap).map((t) => ({ ts: t.ts, t }));
792
+ const all = rec.concat(tl).sort((a, b) => b.ts - a.ts || ((b.e ? b.e.seq : 0) - (a.e ? a.e.seq : 0))).slice(0, cap);
732
793
  return all.map((x, i) => x.e ? evRow(x.e, i === 0 && (S.playing || running)) : telRow(x.t)).join('') || '<div class="empty">Nothing yet.</div>';
733
794
  }
734
795
  function seats(seen, tel) {
@@ -959,6 +1020,7 @@
959
1020
  if (a === 'lv-play') return S.playing ? (stopPlay(), render()) : play();
960
1021
  if (a === 'lv-start') { stopPlay(); S.cursor = 0; return render(); }
961
1022
  if (a === 'lv-live') { stopPlay(); S.cursor = null; return render(); }
1023
+ if (a === 'lv-trace') { S.traceOrder = t.dataset.order || ''; render(); const el = $('#hg-trace-order'); if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' }); return; }
962
1024
  if (a === 'print') return global.print();
963
1025
  if (a === 'md') return download(`${slug}-report.md`, (m.report && m.report.markdown) || '', 'text/markdown');
964
1026
  if (a === 'bundle') return download(`${slug}.bundle.json`, JSON.stringify(src.bundle), 'application/json');
@@ -21,7 +21,7 @@
21
21
  <script>
22
22
  const stored = (() => { try { return localStorage.getItem('hypgraph-theme'); } catch { return null; } })();
23
23
  HypGraph.mount(document.getElementById('app'), {
24
- source: HypGraph.HttpSource('api/graph'), hash: true, theme: stored || 'light',
24
+ source: HypGraph.HttpSource('api/graph'), hash: true, theme: stored || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'),
25
25
  onTheme: (t) => { try { localStorage.setItem('hypgraph-theme', t); } catch { /* private mode */ } },
26
26
  });
27
27
  </script>
@@ -0,0 +1,60 @@
1
+ """The last claims reading 'no live support' after bound results counted: a knowledge claim resting on the sources it
2
+ cites, and claims on results whose author wrote outcome 'failed' for the code under test failing its spec, on runs
3
+ that succeeded (E4: 13 checked claims read UNSUPPORTED — every support has fallen)."""
4
+ import pytest
5
+
6
+ from hypgraph import Graph, Policy
7
+ from hypgraph import derive as D
8
+
9
+
10
+ @pytest.fixture
11
+ def env():
12
+ g = Graph(policy=Policy(reviewers={"critic"}, instrument="lab"))
13
+ inv = g.author("lead").investigation("Support", "Find out what a claim rests on.")["id"]
14
+ return g, inv
15
+
16
+
17
+ def _state(g, nid):
18
+ return D.state(g.snapshot(), nid)
19
+
20
+
21
+ def test_a_cited_source_is_a_line_of_support_rated_external(env):
22
+ g, inv = env
23
+ r = g.author("researcher")
24
+ s1 = r.add("source", inv, citation="config.py in the 2.4.0 release", url="file:sources/config.py",
25
+ passage='"retry_backoff": 30, # seconds; 60 before 2.3.0')["id"]
26
+ s2 = r.add("source", inv, citation="The 2.4 configuration guide", url="file:sources/config-guide.md",
27
+ passage="retry_backoff defaults to 30 seconds")["id"]
28
+ c = r.add("claim", inv, statement="The default retry_backoff in the current release is 30 seconds.",
29
+ scope="The 2.4.0 release's config module and its configuration guide.",
30
+ links=[{"rel": "cites", "to": s1}, {"rel": "cites", "to": s2}])
31
+ st = _state(g, c["id"])
32
+ assert st["independent_lines"] == 2 and st["evidence"] == "external" and not st["unsupported"]
33
+
34
+
35
+ def test_a_failed_outcome_on_a_succeeded_run_is_a_finding_not_a_fall(env):
36
+ g, inv = env
37
+ t = g.author("tester")
38
+ run = t.add("run", inv, kind="agent", name="spec cases", state="succeeded")["id"]
39
+ r = t.add("result", inv, kind="computed", outcome="failed",
40
+ summary="Of the 8 valid-input cases, parse_duration gets exactly 1 wrong: case 8.",
41
+ values={"valid_wrong": {"k": 1, "n": 8}}, links=[{"rel": "produced", "from": run}])["id"]
42
+ assert not D.fallen(g.snapshot(), r)
43
+ c = t.add("claim", inv, statement="parse_duration gets exactly 1 of the 8 valid-input cases wrong.",
44
+ scope="SPEC.md's 8 valid-input cases on the current diff.",
45
+ measurements=[{"name": "valid wrong", "result": r, "key": "valid_wrong", "k": 1, "n": 8}],
46
+ links=[{"rel": "derived_from", "to": r}])
47
+ st = _state(g, c["id"])
48
+ assert st["independent_lines"] == 1 and not st["unsupported"] and "UNSUPPORTED" not in st["sentence"]
49
+
50
+
51
+ def test_a_failed_outcome_still_falls_when_the_run_failed_or_there_is_none(env):
52
+ g, inv = env
53
+ t = g.author("tester")
54
+ bad = t.add("run", inv, kind="agent", name="spec cases", state="failed")["id"]
55
+ r1 = t.add("result", inv, kind="computed", outcome="failed", summary="The harness crashed before the cases ran.",
56
+ values={"n_cases": 12}, links=[{"rel": "produced", "from": bad}])["id"]
57
+ r2 = t.add("result", inv, kind="computed", outcome="failed", summary="pytest could not import the module.",
58
+ values={"n_collected": 0})["id"]
59
+ s = g.snapshot()
60
+ assert D.fallen(s, r1) and D.fallen(s, r2)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes