@oriro/orirocli 0.1.9 → 0.1.12

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 (166) hide show
  1. package/README.md +16 -18
  2. package/dist/cli.js +4776 -2964
  3. package/package.json +2 -2
  4. package/skills/craft/ai-engineering/SKILL.md +2 -2
  5. package/skills/graphify/SKILL.md +0 -619
  6. package/skills/graphify/__init__.py +0 -28
  7. package/skills/graphify/__main__.py +0 -4582
  8. package/skills/graphify/affected.py +0 -154
  9. package/skills/graphify/always_on/agents-md.md +0 -12
  10. package/skills/graphify/always_on/antigravity-rules.md +0 -14
  11. package/skills/graphify/always_on/claude-md.md +0 -9
  12. package/skills/graphify/always_on/gemini-md.md +0 -9
  13. package/skills/graphify/always_on/kiro-steering.md +0 -5
  14. package/skills/graphify/always_on/vscode-instructions.md +0 -17
  15. package/skills/graphify/analyze.py +0 -724
  16. package/skills/graphify/benchmark.py +0 -155
  17. package/skills/graphify/build.py +0 -487
  18. package/skills/graphify/cache.py +0 -417
  19. package/skills/graphify/callflow_html.py +0 -2020
  20. package/skills/graphify/cluster.py +0 -272
  21. package/skills/graphify/command-kilo.md +0 -15
  22. package/skills/graphify/dedup.py +0 -429
  23. package/skills/graphify/detect.py +0 -1379
  24. package/skills/graphify/diagnostics.py +0 -390
  25. package/skills/graphify/export.py +0 -1408
  26. package/skills/graphify/extract.py +0 -11570
  27. package/skills/graphify/global_graph.py +0 -159
  28. package/skills/graphify/google_workspace.py +0 -223
  29. package/skills/graphify/hooks.py +0 -457
  30. package/skills/graphify/ingest.py +0 -331
  31. package/skills/graphify/llm.py +0 -1896
  32. package/skills/graphify/manifest.py +0 -4
  33. package/skills/graphify/mcp_ingest.py +0 -392
  34. package/skills/graphify/multigraph_compat.py +0 -212
  35. package/skills/graphify/pg_introspect.py +0 -142
  36. package/skills/graphify/prs.py +0 -748
  37. package/skills/graphify/querylog.py +0 -70
  38. package/skills/graphify/report.py +0 -218
  39. package/skills/graphify/scip_ingest.py +0 -363
  40. package/skills/graphify/security.py +0 -336
  41. package/skills/graphify/semantic_cleanup.py +0 -319
  42. package/skills/graphify/serve.py +0 -1309
  43. package/skills/graphify/skill-aider.md +0 -1246
  44. package/skills/graphify/skill-amp.md +0 -613
  45. package/skills/graphify/skill-claw.md +0 -616
  46. package/skills/graphify/skill-codex.md +0 -613
  47. package/skills/graphify/skill-copilot.md +0 -616
  48. package/skills/graphify/skill-devin.md +0 -1372
  49. package/skills/graphify/skill-droid.md +0 -613
  50. package/skills/graphify/skill-kilo.md +0 -625
  51. package/skills/graphify/skill-kiro.md +0 -615
  52. package/skills/graphify/skill-opencode.md +0 -608
  53. package/skills/graphify/skill-pi.md +0 -615
  54. package/skills/graphify/skill-trae.md +0 -614
  55. package/skills/graphify/skill-vscode.md +0 -612
  56. package/skills/graphify/skill-windows.md +0 -651
  57. package/skills/graphify/skills/amp/references/add-watch.md +0 -56
  58. package/skills/graphify/skills/amp/references/exports.md +0 -71
  59. package/skills/graphify/skills/amp/references/extraction-spec.md +0 -68
  60. package/skills/graphify/skills/amp/references/github-and-merge.md +0 -46
  61. package/skills/graphify/skills/amp/references/hooks.md +0 -33
  62. package/skills/graphify/skills/amp/references/query.md +0 -249
  63. package/skills/graphify/skills/amp/references/transcribe.md +0 -48
  64. package/skills/graphify/skills/amp/references/update.md +0 -179
  65. package/skills/graphify/skills/claude/references/add-watch.md +0 -56
  66. package/skills/graphify/skills/claude/references/exports.md +0 -71
  67. package/skills/graphify/skills/claude/references/extraction-spec.md +0 -68
  68. package/skills/graphify/skills/claude/references/github-and-merge.md +0 -46
  69. package/skills/graphify/skills/claude/references/hooks.md +0 -33
  70. package/skills/graphify/skills/claude/references/query.md +0 -103
  71. package/skills/graphify/skills/claude/references/transcribe.md +0 -48
  72. package/skills/graphify/skills/claude/references/update.md +0 -179
  73. package/skills/graphify/skills/claw/references/add-watch.md +0 -56
  74. package/skills/graphify/skills/claw/references/exports.md +0 -71
  75. package/skills/graphify/skills/claw/references/extraction-spec.md +0 -29
  76. package/skills/graphify/skills/claw/references/github-and-merge.md +0 -46
  77. package/skills/graphify/skills/claw/references/hooks.md +0 -33
  78. package/skills/graphify/skills/claw/references/query.md +0 -249
  79. package/skills/graphify/skills/claw/references/transcribe.md +0 -48
  80. package/skills/graphify/skills/claw/references/update.md +0 -179
  81. package/skills/graphify/skills/codex/references/add-watch.md +0 -56
  82. package/skills/graphify/skills/codex/references/exports.md +0 -71
  83. package/skills/graphify/skills/codex/references/extraction-spec.md +0 -29
  84. package/skills/graphify/skills/codex/references/github-and-merge.md +0 -46
  85. package/skills/graphify/skills/codex/references/hooks.md +0 -33
  86. package/skills/graphify/skills/codex/references/query.md +0 -249
  87. package/skills/graphify/skills/codex/references/transcribe.md +0 -48
  88. package/skills/graphify/skills/codex/references/update.md +0 -179
  89. package/skills/graphify/skills/copilot/references/add-watch.md +0 -56
  90. package/skills/graphify/skills/copilot/references/exports.md +0 -71
  91. package/skills/graphify/skills/copilot/references/extraction-spec.md +0 -68
  92. package/skills/graphify/skills/copilot/references/github-and-merge.md +0 -46
  93. package/skills/graphify/skills/copilot/references/hooks.md +0 -33
  94. package/skills/graphify/skills/copilot/references/query.md +0 -249
  95. package/skills/graphify/skills/copilot/references/transcribe.md +0 -48
  96. package/skills/graphify/skills/copilot/references/update.md +0 -179
  97. package/skills/graphify/skills/droid/references/add-watch.md +0 -56
  98. package/skills/graphify/skills/droid/references/exports.md +0 -71
  99. package/skills/graphify/skills/droid/references/extraction-spec.md +0 -68
  100. package/skills/graphify/skills/droid/references/github-and-merge.md +0 -46
  101. package/skills/graphify/skills/droid/references/hooks.md +0 -33
  102. package/skills/graphify/skills/droid/references/query.md +0 -249
  103. package/skills/graphify/skills/droid/references/transcribe.md +0 -48
  104. package/skills/graphify/skills/droid/references/update.md +0 -179
  105. package/skills/graphify/skills/kilo/references/add-watch.md +0 -56
  106. package/skills/graphify/skills/kilo/references/exports.md +0 -71
  107. package/skills/graphify/skills/kilo/references/extraction-spec.md +0 -68
  108. package/skills/graphify/skills/kilo/references/github-and-merge.md +0 -46
  109. package/skills/graphify/skills/kilo/references/hooks.md +0 -33
  110. package/skills/graphify/skills/kilo/references/query.md +0 -249
  111. package/skills/graphify/skills/kilo/references/transcribe.md +0 -48
  112. package/skills/graphify/skills/kilo/references/update.md +0 -179
  113. package/skills/graphify/skills/kiro/references/add-watch.md +0 -56
  114. package/skills/graphify/skills/kiro/references/exports.md +0 -71
  115. package/skills/graphify/skills/kiro/references/extraction-spec.md +0 -29
  116. package/skills/graphify/skills/kiro/references/github-and-merge.md +0 -46
  117. package/skills/graphify/skills/kiro/references/hooks.md +0 -33
  118. package/skills/graphify/skills/kiro/references/query.md +0 -249
  119. package/skills/graphify/skills/kiro/references/transcribe.md +0 -48
  120. package/skills/graphify/skills/kiro/references/update.md +0 -179
  121. package/skills/graphify/skills/opencode/references/add-watch.md +0 -56
  122. package/skills/graphify/skills/opencode/references/exports.md +0 -71
  123. package/skills/graphify/skills/opencode/references/extraction-spec.md +0 -68
  124. package/skills/graphify/skills/opencode/references/github-and-merge.md +0 -46
  125. package/skills/graphify/skills/opencode/references/hooks.md +0 -33
  126. package/skills/graphify/skills/opencode/references/query.md +0 -249
  127. package/skills/graphify/skills/opencode/references/transcribe.md +0 -48
  128. package/skills/graphify/skills/opencode/references/update.md +0 -179
  129. package/skills/graphify/skills/pi/references/add-watch.md +0 -56
  130. package/skills/graphify/skills/pi/references/exports.md +0 -71
  131. package/skills/graphify/skills/pi/references/extraction-spec.md +0 -29
  132. package/skills/graphify/skills/pi/references/github-and-merge.md +0 -46
  133. package/skills/graphify/skills/pi/references/hooks.md +0 -33
  134. package/skills/graphify/skills/pi/references/query.md +0 -249
  135. package/skills/graphify/skills/pi/references/transcribe.md +0 -48
  136. package/skills/graphify/skills/pi/references/update.md +0 -179
  137. package/skills/graphify/skills/trae/references/add-watch.md +0 -56
  138. package/skills/graphify/skills/trae/references/exports.md +0 -71
  139. package/skills/graphify/skills/trae/references/extraction-spec.md +0 -68
  140. package/skills/graphify/skills/trae/references/github-and-merge.md +0 -46
  141. package/skills/graphify/skills/trae/references/hooks.md +0 -35
  142. package/skills/graphify/skills/trae/references/query.md +0 -249
  143. package/skills/graphify/skills/trae/references/transcribe.md +0 -48
  144. package/skills/graphify/skills/trae/references/update.md +0 -179
  145. package/skills/graphify/skills/vscode/references/add-watch.md +0 -56
  146. package/skills/graphify/skills/vscode/references/exports.md +0 -71
  147. package/skills/graphify/skills/vscode/references/extraction-spec.md +0 -68
  148. package/skills/graphify/skills/vscode/references/github-and-merge.md +0 -46
  149. package/skills/graphify/skills/vscode/references/hooks.md +0 -33
  150. package/skills/graphify/skills/vscode/references/query.md +0 -249
  151. package/skills/graphify/skills/vscode/references/transcribe.md +0 -48
  152. package/skills/graphify/skills/vscode/references/update.md +0 -179
  153. package/skills/graphify/skills/windows/references/add-watch.md +0 -56
  154. package/skills/graphify/skills/windows/references/exports.md +0 -71
  155. package/skills/graphify/skills/windows/references/extraction-spec.md +0 -68
  156. package/skills/graphify/skills/windows/references/github-and-merge.md +0 -46
  157. package/skills/graphify/skills/windows/references/hooks.md +0 -33
  158. package/skills/graphify/skills/windows/references/query.md +0 -249
  159. package/skills/graphify/skills/windows/references/transcribe.md +0 -48
  160. package/skills/graphify/skills/windows/references/update.md +0 -179
  161. package/skills/graphify/symbol_resolution.py +0 -538
  162. package/skills/graphify/transcribe.py +0 -184
  163. package/skills/graphify/tree_html.py +0 -582
  164. package/skills/graphify/validate.py +0 -72
  165. package/skills/graphify/watch.py +0 -898
  166. package/skills/graphify/wiki.py +0 -282
@@ -1,390 +0,0 @@
1
- """Read-only diagnostics for MultiDiGraph readiness."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import re
7
- from collections import Counter, defaultdict
8
- from copy import deepcopy
9
- from pathlib import Path
10
- from typing import Any
11
-
12
- import networkx as nx
13
-
14
-
15
- _SUPPRESSION_DECL_RE = re.compile(r"^\s*(?P<name>seen_[A-Za-z0-9_]+)\s*[:=]")
16
- _TYPE_TUPLE_RE = re.compile(r"set\[tuple\[(?P<inside>[^\]]+)\]\]")
17
-
18
-
19
- def _safe_text(value: Any) -> str:
20
- if value is None:
21
- return ""
22
- if isinstance(value, (str, int, float, bool)):
23
- return str(value)
24
- return json.dumps(value, sort_keys=True, default=str, ensure_ascii=False)
25
-
26
-
27
- def _edge_list(extraction: dict[str, Any]) -> list[Any]:
28
- edges = extraction.get("edges")
29
- if edges is None:
30
- edges = extraction.get("links")
31
- return edges if isinstance(edges, list) else []
32
-
33
-
34
- def _node_ids(extraction: dict[str, Any]) -> set[str]:
35
- nodes = extraction.get("nodes", [])
36
- if not isinstance(nodes, list):
37
- return set()
38
- return {
39
- str(node["id"])
40
- for node in nodes
41
- if isinstance(node, dict) and "id" in node and node.get("id") is not None
42
- }
43
-
44
-
45
- def _canonical_edge(edge: Any) -> dict[str, str]:
46
- if not isinstance(edge, dict):
47
- return {
48
- "source": "",
49
- "target": "",
50
- "relation": "",
51
- "confidence": "",
52
- "source_file": "",
53
- "source_location": "",
54
- "context": "",
55
- "_invalid": "non_object_edge",
56
- }
57
- source = edge.get("source", edge.get("from"))
58
- target = edge.get("target", edge.get("to"))
59
- return {
60
- "source": _safe_text(source),
61
- "target": _safe_text(target),
62
- "relation": _safe_text(edge.get("relation")),
63
- "confidence": _safe_text(edge.get("confidence")),
64
- "source_file": _safe_text(edge.get("source_file")),
65
- "source_location": _safe_text(edge.get("source_location")),
66
- "context": _safe_text(edge.get("context")),
67
- "_invalid": "",
68
- }
69
-
70
-
71
- def _exact_signature(edge: Any) -> str:
72
- if not isinstance(edge, dict):
73
- return "<non-object>"
74
- normalized = dict(edge)
75
- if "source" not in normalized and "from" in normalized:
76
- normalized["source"] = normalized["from"]
77
- if "target" not in normalized and "to" in normalized:
78
- normalized["target"] = normalized["to"]
79
- normalized.pop("from", None)
80
- normalized.pop("to", None)
81
- return json.dumps(
82
- normalized,
83
- sort_keys=True,
84
- default=str,
85
- ensure_ascii=False,
86
- separators=(",", ":"),
87
- )
88
-
89
-
90
- def _count_extra(counter: Counter[Any]) -> int:
91
- return sum(count - 1 for count in counter.values() if count > 1)
92
-
93
-
94
- def _variant_group_count(
95
- grouped_edges: dict[tuple[str, str], list[dict[str, str]]],
96
- field: str,
97
- *,
98
- relation_sensitive: bool = False,
99
- ) -> int:
100
- groups = 0
101
- for edges in grouped_edges.values():
102
- if relation_sensitive:
103
- by_relation: dict[str, set[str]] = defaultdict(set)
104
- for edge in edges:
105
- by_relation[edge["relation"]].add(edge[field])
106
- groups += sum(1 for values in by_relation.values() if len(values) > 1)
107
- elif len({edge[field] for edge in edges}) > 1:
108
- groups += 1
109
- return groups
110
-
111
-
112
- def _tuple_arity_from_annotation(line: str) -> int:
113
- match = _TYPE_TUPLE_RE.search(line)
114
- if not match:
115
- return 0
116
- inside = match.group("inside").strip()
117
- if not inside:
118
- return 0
119
- return inside.count(",") + 1
120
-
121
-
122
- def scan_producer_suppression_sites(path: str | Path) -> dict[str, Any]:
123
- """Find likely `seen_*` producer-suppression sets in an extractor file."""
124
- source_path = Path(path)
125
- if not source_path.exists():
126
- return {
127
- "path": str(source_path),
128
- "total_sites": 0,
129
- "sites": [],
130
- "error": "file not found",
131
- }
132
-
133
- sites: list[dict[str, Any]] = []
134
- lines = source_path.read_text(encoding="utf-8").splitlines()
135
- for lineno, line in enumerate(lines, start=1):
136
- match = _SUPPRESSION_DECL_RE.match(line)
137
- if not match:
138
- continue
139
- sites.append(
140
- {
141
- "line": lineno,
142
- "name": match.group("name"),
143
- "tuple_arity": _tuple_arity_from_annotation(line),
144
- "sample": line.strip()[:120],
145
- }
146
- )
147
-
148
- return {
149
- "path": str(source_path),
150
- "total_sites": len(sites),
151
- "sites": sites,
152
- "error": "",
153
- }
154
-
155
-
156
- def diagnose_extraction(
157
- extraction: dict[str, Any],
158
- *,
159
- directed: bool = True,
160
- root: str | Path | None = None,
161
- max_examples: int = 5,
162
- extract_path: str | Path | None = None,
163
- ) -> dict[str, Any]:
164
- """Summarize same-endpoint edge-collapse risk for one JSON graph/extraction dict."""
165
- from graphify.build import build_from_json
166
-
167
- node_ids = _node_ids(extraction)
168
- raw_edges = _edge_list(extraction)
169
- canonical_edges = [_canonical_edge(edge) for edge in raw_edges]
170
-
171
- exact_counts: Counter[str] = Counter(_exact_signature(edge) for edge in raw_edges)
172
- directed_pairs: Counter[tuple[str, str]] = Counter()
173
- undirected_pairs: Counter[tuple[str, str]] = Counter()
174
- grouped: dict[tuple[str, str], list[dict[str, str]]] = defaultdict(list)
175
-
176
- non_object_edges = 0
177
- missing_endpoint_edges = 0
178
- dangling_endpoint_edges = 0
179
- self_loop_edges = 0
180
- valid_candidate_edges = 0
181
-
182
- for edge in canonical_edges:
183
- if edge["_invalid"]:
184
- non_object_edges += 1
185
- continue
186
- source = edge["source"]
187
- target = edge["target"]
188
- if not source or not target:
189
- missing_endpoint_edges += 1
190
- continue
191
- if source not in node_ids or target not in node_ids:
192
- dangling_endpoint_edges += 1
193
- continue
194
- if source == target:
195
- self_loop_edges += 1
196
- valid_candidate_edges += 1
197
- directed_pair = (source, target)
198
- undirected_pair = (source, target) if source <= target else (target, source)
199
- directed_pairs[directed_pair] += 1
200
- undirected_pairs[undirected_pair] += 1
201
- grouped[directed_pair].append(edge)
202
-
203
- examples: list[dict[str, Any]] = []
204
- if max_examples > 0:
205
- for (source, target), count in directed_pairs.most_common():
206
- if count < 2:
207
- continue
208
- edges = grouped[(source, target)]
209
- examples.append(
210
- {
211
- "source": source,
212
- "target": target,
213
- "edge_count": count,
214
- "relations": sorted({edge["relation"] for edge in edges}),
215
- "source_files": sorted({edge["source_file"] for edge in edges}),
216
- "source_locations": sorted({edge["source_location"] for edge in edges}),
217
- "contexts": sorted({edge["context"] for edge in edges}),
218
- }
219
- )
220
- if len(examples) >= max_examples:
221
- break
222
-
223
- build_error = ""
224
- graph_type = ""
225
- post_build_edge_count: int | None = None
226
- post_build_node_count: int | None = None
227
- try:
228
- graph_input = deepcopy(extraction)
229
- graph: nx.Graph = build_from_json(graph_input, directed=directed, root=root)
230
- graph_type = type(graph).__name__
231
- post_build_edge_count = graph.number_of_edges()
232
- post_build_node_count = graph.number_of_nodes()
233
- except Exception as exc:
234
- build_error = f"{type(exc).__name__}: {exc}"
235
-
236
- suppression_path = (
237
- Path(extract_path) if extract_path else Path(__file__).with_name("extract.py")
238
- )
239
-
240
- return {
241
- "node_count": len(node_ids),
242
- "raw_edge_count": len(raw_edges),
243
- "non_object_edges": non_object_edges,
244
- "missing_endpoint_edges": missing_endpoint_edges,
245
- "dangling_endpoint_edges": dangling_endpoint_edges,
246
- "self_loop_edges": self_loop_edges,
247
- "valid_candidate_edges": valid_candidate_edges,
248
- "exact_duplicate_edges": _count_extra(exact_counts),
249
- "directed_unique_endpoint_pairs": len(directed_pairs),
250
- "directed_same_endpoint_collapsed_edges": _count_extra(directed_pairs),
251
- "undirected_unique_endpoint_pairs": len(undirected_pairs),
252
- "undirected_same_endpoint_collapsed_edges": _count_extra(undirected_pairs),
253
- "same_endpoint_group_count": sum(1 for count in directed_pairs.values() if count > 1),
254
- "relation_variant_groups": _variant_group_count(grouped, "relation"),
255
- "source_file_variant_groups": _variant_group_count(
256
- grouped, "source_file", relation_sensitive=True
257
- ),
258
- "source_location_variant_groups": _variant_group_count(
259
- grouped, "source_location", relation_sensitive=True
260
- ),
261
- "context_variant_groups": _variant_group_count(grouped, "context", relation_sensitive=True),
262
- "post_build_graph_type": graph_type,
263
- "post_build_node_count": post_build_node_count,
264
- "post_build_edge_count": post_build_edge_count,
265
- "post_build_error": build_error,
266
- "producer_suppression": scan_producer_suppression_sites(suppression_path),
267
- "examples": examples,
268
- }
269
-
270
-
271
- def _read_json_file(path: str | Path) -> dict[str, Any]:
272
- """Read a JSON graph after applying Graphify's graph-load size cap."""
273
- from graphify.security import check_graph_file_size_cap
274
-
275
- json_path = Path(path)
276
- check_graph_file_size_cap(json_path)
277
- data = json.loads(json_path.read_text(encoding="utf-8"))
278
- if not isinstance(data, dict):
279
- raise ValueError("diagnostic input must be a JSON object")
280
- return data
281
-
282
-
283
- def diagnose_file(
284
- path: str | Path,
285
- *,
286
- directed: bool | None = None,
287
- root: str | Path | None = None,
288
- max_examples: int = 5,
289
- extract_path: str | Path | None = None,
290
- ) -> dict[str, Any]:
291
- """Diagnose a graph/extraction JSON file without mutating it.
292
-
293
- When `directed` is None, the JSON's "directed" flag is honored. Raw
294
- extraction JSON that has no "directed" flag defaults to directed analysis.
295
- """
296
- data = _read_json_file(path)
297
- if directed is None:
298
- raw_directed = data.get("directed")
299
- effective_directed = raw_directed if isinstance(raw_directed, bool) else True
300
- else:
301
- effective_directed = directed
302
-
303
- summary = diagnose_extraction(
304
- data,
305
- directed=effective_directed,
306
- root=root,
307
- max_examples=max_examples,
308
- extract_path=extract_path,
309
- )
310
- summary["input_path"] = str(path)
311
- summary["effective_directed"] = effective_directed
312
- return summary
313
-
314
-
315
- def format_diagnostic_json(summary: dict[str, Any]) -> dict[str, Any]:
316
- return {
317
- "schema_version": 1,
318
- "summary": {
319
- key: value
320
- for key, value in summary.items()
321
- if key not in {"examples", "producer_suppression"}
322
- },
323
- "examples": summary.get("examples", []),
324
- "producer_suppression": summary.get("producer_suppression", {}),
325
- "notes": [
326
- "Diagnostics are read-only.",
327
- "A normal graph.json is already post-build and cannot recover raw producer edges.",
328
- "Producer suppression sites are heuristic source-code evidence.",
329
- ],
330
- }
331
-
332
-
333
- def format_diagnostic_report(summary: dict[str, Any]) -> str:
334
- suppression = summary.get("producer_suppression", {})
335
- lines = [
336
- "[graphify] MultiDiGraph edge-collapse diagnostic",
337
- f"input: {summary.get('input_path', '<in-memory>')}",
338
- "input_stage: provided JSON (normal graph.json is post-build)",
339
- f"effective_directed: {summary.get('effective_directed', '<direct-call>')}",
340
- f"nodes: {summary['node_count']}",
341
- f"raw_edges: {summary['raw_edge_count']}",
342
- f"valid_candidate_edges: {summary['valid_candidate_edges']}",
343
- f"missing_endpoint_edges: {summary['missing_endpoint_edges']}",
344
- f"dangling_endpoint_edges: {summary['dangling_endpoint_edges']}",
345
- f"self_loop_edges: {summary['self_loop_edges']}",
346
- f"exact_duplicate_edges: {summary['exact_duplicate_edges']}",
347
- f"directed_unique_endpoint_pairs: {summary['directed_unique_endpoint_pairs']}",
348
- (
349
- "directed_same_endpoint_collapsed_edges: "
350
- f"{summary['directed_same_endpoint_collapsed_edges']}"
351
- ),
352
- f"undirected_unique_endpoint_pairs: {summary['undirected_unique_endpoint_pairs']}",
353
- (
354
- "undirected_same_endpoint_collapsed_edges: "
355
- f"{summary['undirected_same_endpoint_collapsed_edges']}"
356
- ),
357
- f"same_endpoint_group_count: {summary['same_endpoint_group_count']}",
358
- f"relation_variant_groups: {summary['relation_variant_groups']}",
359
- f"source_file_variant_groups: {summary['source_file_variant_groups']}",
360
- f"source_location_variant_groups: {summary['source_location_variant_groups']}",
361
- f"context_variant_groups: {summary['context_variant_groups']}",
362
- f"post_build_graph_type: {summary['post_build_graph_type']}",
363
- f"post_build_edges: {summary['post_build_edge_count']}",
364
- f"producer_suppression_sites: {suppression.get('total_sites', 0)}",
365
- ]
366
- if summary.get("post_build_error"):
367
- lines.append(f"post_build_error: {summary['post_build_error']}")
368
- if suppression.get("error"):
369
- lines.append(f"producer_suppression_error: {suppression['error']}")
370
- if suppression.get("sites"):
371
- lines.append("producer_suppression_examples:")
372
- for site in suppression["sites"][:8]:
373
- lines.append(
374
- f" - L{site['line']} {site['name']} arity={site['tuple_arity'] or 'unknown'}"
375
- )
376
- if summary.get("examples"):
377
- lines.append("examples:")
378
- for example in summary["examples"]:
379
- lines.append(
380
- " - "
381
- f"{example['source']} -> {example['target']} "
382
- f"edges={example['edge_count']} "
383
- f"relations={example['relations']} "
384
- f"locations={example['source_locations']} "
385
- f"contexts={example['contexts']}"
386
- )
387
- lines.append(
388
- "note: normal graph.json is post-build; raw producer loss must be measured earlier."
389
- )
390
- return "\n".join(lines)