codebase-explorer 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cbe/__init__.py +9 -0
- cbe/__main__.py +4 -0
- cbe/aggregation.py +183 -0
- cbe/api_completion.py +410 -0
- cbe/batching.py +283 -0
- cbe/catalog_query.py +376 -0
- cbe/checks.py +390 -0
- cbe/cli.py +434 -0
- cbe/concrete_facts.py +321 -0
- cbe/data/LICENSE.tiktoken +30 -0
- cbe/data/o200k_base.tiktoken +199998 -0
- cbe/declarations.py +537 -0
- cbe/docs.py +929 -0
- cbe/generate.py +316 -0
- cbe/grammars.py +741 -0
- cbe/graph.py +857 -0
- cbe/graph_backends/__init__.py +43 -0
- cbe/graph_backends/_calls.py +262 -0
- cbe/graph_backends/_paths.py +42 -0
- cbe/graph_backends/identity.py +93 -0
- cbe/graph_backends/imports.py +256 -0
- cbe/graph_backends/joern.py +1021 -0
- cbe/graph_backends/resolution.py +288 -0
- cbe/graph_backends/toolchain.py +396 -0
- cbe/grounding.py +57 -0
- cbe/host_run.py +1752 -0
- cbe/model_settings.py +80 -0
- cbe/projection.py +305 -0
- cbe/prompts.py +316 -0
- cbe/records.py +200 -0
- cbe/skeleton.py +474 -0
- cbe/source_context.py +709 -0
- cbe/symbols.py +136 -0
- cbe/token_budget.py +68 -0
- cbe/treename.py +398 -0
- cbe/usage.py +300 -0
- cbe_install/__init__.py +11 -0
- cbe_install/cli.py +434 -0
- cbe_install/resources/agents/cbe-writer.md +16 -0
- cbe_install/resources/codebase-explorer/SKILL.md +101 -0
- cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
- cbe_install/resources/codebase-explorer/references/codex.md +37 -0
- codebase_explorer-2.2.0.dist-info/METADATA +233 -0
- codebase_explorer-2.2.0.dist-info/RECORD +47 -0
- codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
- codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
- codebase_explorer-2.2.0.dist-info/licenses/LICENSE +21 -0
cbe/model_settings.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""API model resource defaults and user configuration, in one place.
|
|
2
|
+
|
|
3
|
+
Exact model ids opt in; unknown models retain the historical API behavior.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from cbe import batching
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_API_CONFIG: dict[str, Any] = {
|
|
16
|
+
"base_url": "https://api.openai.com/v1",
|
|
17
|
+
"api_key_env": "OPENAI_API_KEY",
|
|
18
|
+
"api_key_file": None,
|
|
19
|
+
"extra_headers": {},
|
|
20
|
+
}
|
|
21
|
+
#: Provider request fields sent only when configured; ``api_completion`` reads exactly these.
|
|
22
|
+
OPTIONAL_REQUEST_KEYS = ("response_format", "top_p", "reasoning_effort")
|
|
23
|
+
#: Every config key the code reads. ``load_config`` rejects anything else.
|
|
24
|
+
ALLOWED_API_CONFIG_KEYS = frozenset(DEFAULT_API_CONFIG) | frozenset(OPTIONAL_REQUEST_KEYS) | {
|
|
25
|
+
"timeout", "output_max_tokens", "batch_target_tokens", "batch_max_tokens",
|
|
26
|
+
"temperature", "price_per_million",
|
|
27
|
+
}
|
|
28
|
+
MODEL_DEFAULTS: dict[str, dict[str, Any]] = {
|
|
29
|
+
"zai/glm-5.3-flash": {
|
|
30
|
+
"timeout": 1800,
|
|
31
|
+
"output_max_tokens": 49152,
|
|
32
|
+
"reasoning_effort": "low",
|
|
33
|
+
"response_format": {"type": "json_object"},
|
|
34
|
+
"batch_target_tokens": 8000,
|
|
35
|
+
"batch_max_tokens": 10000,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ApiConfigError(ValueError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def model_defaults(model: str) -> dict[str, Any]:
|
|
45
|
+
"""Defaults for a model: the exact name, else the same model under another gateway prefix."""
|
|
46
|
+
if model in MODEL_DEFAULTS:
|
|
47
|
+
return MODEL_DEFAULTS[model]
|
|
48
|
+
name = model.rsplit("/", 1)[-1]
|
|
49
|
+
return next((row for key, row in MODEL_DEFAULTS.items() if key.rsplit("/", 1)[-1] == name), {})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_config(path: Path, model: str = "") -> dict[str, Any]:
|
|
53
|
+
"""Merge transport defaults < model defaults < user JSON settings."""
|
|
54
|
+
value = {**DEFAULT_API_CONFIG, **model_defaults(model)}
|
|
55
|
+
if path.is_file():
|
|
56
|
+
try:
|
|
57
|
+
loaded = json.loads(path.read_text(encoding="utf-8"))
|
|
58
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
59
|
+
raise ApiConfigError(f"API config is not valid JSON: {exc}") from exc
|
|
60
|
+
if not isinstance(loaded, dict):
|
|
61
|
+
raise ApiConfigError("API config must be a JSON object")
|
|
62
|
+
unknown = sorted(set(loaded) - ALLOWED_API_CONFIG_KEYS)
|
|
63
|
+
if unknown:
|
|
64
|
+
raise ApiConfigError(f"unknown API config keys: {', '.join(unknown)}")
|
|
65
|
+
value.update(loaded)
|
|
66
|
+
for key in ("timeout", "output_max_tokens", "batch_target_tokens", "batch_max_tokens"):
|
|
67
|
+
if key in value and (type(value[key]) is not int or value[key] < 1):
|
|
68
|
+
raise ApiConfigError(f"{key} must be a positive integer")
|
|
69
|
+
batch_max = value.get("batch_max_tokens", batching.MAX_TOKENS)
|
|
70
|
+
target = value.get("batch_target_tokens", min(batching.TARGET_TOKENS, batch_max))
|
|
71
|
+
if target > batch_max:
|
|
72
|
+
raise ApiConfigError("batch_target_tokens must not exceed batch_max_tokens")
|
|
73
|
+
if "batch_max_tokens" in value:
|
|
74
|
+
value.setdefault("batch_target_tokens", target)
|
|
75
|
+
rates = value.get("price_per_million")
|
|
76
|
+
if rates is not None:
|
|
77
|
+
if (not isinstance(rates, list) or len(rates) != 3 or
|
|
78
|
+
any(type(rate) not in (int, float) or not math.isfinite(rate) or rate < 0 for rate in rates)):
|
|
79
|
+
raise ApiConfigError("price_per_million needs three finite nonnegative rates: input,output,cache")
|
|
80
|
+
return value
|
cbe/projection.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Unit-level graph evidence for detail authors, shared by both drivers.
|
|
2
|
+
|
|
3
|
+
The inventory defines identity; chunks define call-site ownership. Rendering and
|
|
4
|
+
budget selection live here so host files and all detail/review/repair prompts
|
|
5
|
+
cannot acquire different projection rules.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import Counter, defaultdict
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from cbe.records import SourceOwnership
|
|
15
|
+
from cbe.token_budget import count_text_tokens
|
|
16
|
+
|
|
17
|
+
DEFAULT_GRAPH_MAX_TOKENS = 3000
|
|
18
|
+
|
|
19
|
+
# Joern names observed in the O1c Flask and O1b cjson/polyglot graphs;
|
|
20
|
+
# C3b's independent cfg_check.py also documents <clinit>. Match only known
|
|
21
|
+
# terminal suffixes: <redefined>, <duplicate>, <const> and <init> are real
|
|
22
|
+
# source methods, and a parent <lambda> must not hide a named inner function.
|
|
23
|
+
_SYNTHETIC_CFG_OWNER = re.compile(
|
|
24
|
+
r"<(?:lambda|metaClassAdapter|metaClassCallHandler|fakeNew|module|body|"
|
|
25
|
+
r"main|global|clinit)>\d*$"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def graph_state(graph: Any) -> tuple[str, str]:
|
|
30
|
+
"""Use the graph module's authoritative availability classification."""
|
|
31
|
+
from cbe.graph import graph_state as classify_graph
|
|
32
|
+
|
|
33
|
+
return classify_graph(graph)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class EvidenceRow:
|
|
38
|
+
category: str
|
|
39
|
+
text: str
|
|
40
|
+
count: int = 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class UnitGraph:
|
|
45
|
+
"""Unbounded rows in priority order, plus non-optional mapping accounting."""
|
|
46
|
+
rows: tuple[EvidenceRow, ...]
|
|
47
|
+
internal_calls: int
|
|
48
|
+
unmapped_calls: int
|
|
49
|
+
unmapped_cfg: int = 0
|
|
50
|
+
exception_exits_exported: bool = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def unit_graph(graph: dict, paths: list[str], inventory: dict[str, dict],
|
|
54
|
+
chunks: list[dict]) -> UnitGraph:
|
|
55
|
+
"""Map call coordinates into inventory chunks' half-open byte ranges.
|
|
56
|
+
|
|
57
|
+
Frozen chunk text reconstructs source coordinates, including UTF-8 columns.
|
|
58
|
+
Missing coordinates or source bytes are counted rather than guessed by line.
|
|
59
|
+
"""
|
|
60
|
+
wanted = set(paths)
|
|
61
|
+
nodes = {n["id"]: n for n in graph.get("nodes", [])}
|
|
62
|
+
ids = {s["id"] for entry in inventory.values() for s in entry["symbols"]}
|
|
63
|
+
parts: dict[str, list[dict]] = defaultdict(list)
|
|
64
|
+
for chunk in chunks:
|
|
65
|
+
parts[chunk["path"]].append(chunk)
|
|
66
|
+
sources = {path: b"".join(c.get("text", "").encode("utf-8")
|
|
67
|
+
for c in sorted(rows, key=lambda c: c.get("start_byte", 0))).decode("utf-8")
|
|
68
|
+
for path, rows in parts.items()}
|
|
69
|
+
at = SourceOwnership(sources, chunks, ids).at
|
|
70
|
+
|
|
71
|
+
def endpoint(node: dict) -> str | None:
|
|
72
|
+
return at(node.get("path"), node.get("start_line"))
|
|
73
|
+
|
|
74
|
+
outgoing, incoming, ambiguous = [], [], []
|
|
75
|
+
dependencies: dict[str, set[str]] = defaultdict(set)
|
|
76
|
+
external_calls: Counter = Counter()
|
|
77
|
+
unresolved: dict[str, list[str]] = defaultdict(list)
|
|
78
|
+
skeleton: Counter = Counter()
|
|
79
|
+
internal = unmapped = 0
|
|
80
|
+
for edge in sorted(graph.get("edges", []), key=lambda e: str(e.get("id", ""))):
|
|
81
|
+
source = nodes.get(edge.get("source"), {})
|
|
82
|
+
target = nodes.get(edge.get("target"), {})
|
|
83
|
+
path = edge.get("path") or source.get("path")
|
|
84
|
+
target_path = target.get("path")
|
|
85
|
+
local_source, local_target = path in wanted, target_path in wanted
|
|
86
|
+
if edge.get("kind") == "import":
|
|
87
|
+
if edge.get("status") == "external" and local_source:
|
|
88
|
+
dependencies[path].add(str(target.get("name") or edge.get("target_expression")
|
|
89
|
+
or edge.get("expression") or "unknown"))
|
|
90
|
+
if edge.get("status") == "resolved" and path and target_path:
|
|
91
|
+
left = path.rpartition("/")[0] or "."
|
|
92
|
+
right = target_path.rpartition("/")[0] or "."
|
|
93
|
+
skeleton[left, right] += 1
|
|
94
|
+
continue
|
|
95
|
+
if edge.get("kind") != "call" or not (local_source or local_target):
|
|
96
|
+
continue
|
|
97
|
+
caller = at(path, edge.get("line"), edge.get("column"))
|
|
98
|
+
callee = endpoint(target)
|
|
99
|
+
status = edge.get("status")
|
|
100
|
+
if status == "resolved":
|
|
101
|
+
if not caller or not callee:
|
|
102
|
+
unmapped += 1
|
|
103
|
+
continue
|
|
104
|
+
if local_source and local_target:
|
|
105
|
+
internal += 1
|
|
106
|
+
elif local_source:
|
|
107
|
+
outgoing.append((caller, callee, target_path, target.get("start_line")))
|
|
108
|
+
else:
|
|
109
|
+
incoming.append((callee, caller, path, edge.get("line")))
|
|
110
|
+
elif status == "ambiguous":
|
|
111
|
+
candidates = []
|
|
112
|
+
for candidate in (edge.get("candidates") or [])[:3]:
|
|
113
|
+
node = nodes.get(candidate, {}) if isinstance(candidate, str) else candidate
|
|
114
|
+
candidates.append(endpoint(node) or str(node.get("name") or candidate))
|
|
115
|
+
ambiguous.append(EvidenceRow("ambiguous", f"? {caller or path}: "
|
|
116
|
+
f"{edge.get('expression') or edge.get('target_expression') or '?'} => "
|
|
117
|
+
+ "; ".join(candidates) + f" ({len(edge.get('candidates') or [])} candidates)"))
|
|
118
|
+
elif status == "external" and local_source:
|
|
119
|
+
external_calls[path] += 1
|
|
120
|
+
elif status == "unresolved" and local_source:
|
|
121
|
+
expression = str(edge.get("target_expression") or edge.get("expression") or "?")
|
|
122
|
+
unresolved[path].append(expression.split("(", 1)[0].strip()[:80])
|
|
123
|
+
|
|
124
|
+
rows: list[EvidenceRow] = []
|
|
125
|
+
# Group by caller/callee without duplicating an inventory id for every site.
|
|
126
|
+
# Repeated sites remain counted even if their endpoint pair is the same.
|
|
127
|
+
for category, arrow, relations in (("out", "→", outgoing), ("in", "←", incoming)):
|
|
128
|
+
grouped: dict[tuple, Counter] = defaultdict(Counter)
|
|
129
|
+
for local, remote, remote_path, line in relations:
|
|
130
|
+
grouped[local][remote, remote_path, line] += 1
|
|
131
|
+
for local, destinations in sorted(grouped.items()):
|
|
132
|
+
for (remote, remote_path, line), count in sorted(destinations.items()):
|
|
133
|
+
# Each row is independently truncatable; repeated local ids are
|
|
134
|
+
# compressed by render_rows below, keeping omission counts exact.
|
|
135
|
+
rows.append(EvidenceRow(category, f"{local} {arrow} {remote} ({remote_path}:{line})"
|
|
136
|
+
+ (f" ×{count}" if count > 1 else ""), count))
|
|
137
|
+
rows += sorted(ambiguous, key=lambda r: r.text)
|
|
138
|
+
for path in sorted(wanted):
|
|
139
|
+
names = ", ".join(sorted(dependencies[path])) or "none"
|
|
140
|
+
rows.append(EvidenceRow("dependencies", f"External imports {path}: {names}; "
|
|
141
|
+
f"external calls={external_calls[path]}",
|
|
142
|
+
len(dependencies[path]) + external_calls[path]))
|
|
143
|
+
if unresolved[path]:
|
|
144
|
+
names = list(dict.fromkeys(unresolved[path]))[:5]
|
|
145
|
+
rows.append(EvidenceRow("unresolved", f"Unresolved calls {path}: {len(unresolved[path])}; "
|
|
146
|
+
+ "; ".join(names), len(unresolved[path])))
|
|
147
|
+
|
|
148
|
+
symbol_kinds = {s["id"]: s.get("kind") for entry in inventory.values() for s in entry["symbols"]}
|
|
149
|
+
cfg_nodes: dict[str, list[dict]] = defaultdict(list)
|
|
150
|
+
unmapped_owners = set()
|
|
151
|
+
for node in nodes.values():
|
|
152
|
+
if node.get("kind") == "cfg_node" and node.get("path") in wanted:
|
|
153
|
+
owner = nodes.get(node.get("owner"), {})
|
|
154
|
+
method_name = str(owner.get("name", "")).rsplit(".", 1)[-1]
|
|
155
|
+
if _SYNTHETIC_CFG_OWNER.search(method_name):
|
|
156
|
+
continue
|
|
157
|
+
uid = endpoint(owner)
|
|
158
|
+
if uid and symbol_kinds.get(uid) in {"function", "method"}:
|
|
159
|
+
cfg_nodes[uid].append(node)
|
|
160
|
+
else:
|
|
161
|
+
unmapped_owners.add(node.get("owner"))
|
|
162
|
+
for uid, members in sorted(cfg_nodes.items()):
|
|
163
|
+
controls = sum(n.get("label") == "CONTROL_STRUCTURE" for n in members)
|
|
164
|
+
returns = sum(n.get("label") == "RETURN" for n in members)
|
|
165
|
+
if controls or returns:
|
|
166
|
+
rows.append(EvidenceRow("cfg", f"CFG {uid}: control_structures={controls}, "
|
|
167
|
+
f"returns={returns}"))
|
|
168
|
+
# A package skeleton is deliberately small; preserve its omitted pair count.
|
|
169
|
+
for (left, right), count in sorted(skeleton.items())[:12]:
|
|
170
|
+
rows.append(EvidenceRow("packages", f"Package imports {left} → {right}: {count}"))
|
|
171
|
+
if len(skeleton) > 12:
|
|
172
|
+
rows.append(EvidenceRow("packages", f"Package imports: {len(skeleton) - 12} other package pairs",
|
|
173
|
+
len(skeleton) - 12))
|
|
174
|
+
if not skeleton:
|
|
175
|
+
rows.append(EvidenceRow("packages", "Package imports: no resolved package links", 0))
|
|
176
|
+
# Package context survives before lower-priority per-function cfg hints.
|
|
177
|
+
rows.sort(key=lambda row: {"out": 0, "in": 1, "ambiguous": 2,
|
|
178
|
+
"dependencies": 3, "unresolved": 3,
|
|
179
|
+
"packages": 4, "cfg": 5}[row.category])
|
|
180
|
+
return UnitGraph(tuple(rows), internal, unmapped, len(unmapped_owners),
|
|
181
|
+
any(n.get("label") == "THROW" for n in nodes.values()))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def render_rows(rows: list[EvidenceRow]) -> list[str]:
|
|
185
|
+
"""Group adjacent relations by local inventory id, without new identities."""
|
|
186
|
+
lines: list[str] = []
|
|
187
|
+
previous = None
|
|
188
|
+
for row in rows:
|
|
189
|
+
if row.category in {"out", "in"}:
|
|
190
|
+
arrow = " → " if row.category == "out" else " ← "
|
|
191
|
+
local, remote = row.text.split(arrow, 1)
|
|
192
|
+
key = row.category, local
|
|
193
|
+
if previous == key:
|
|
194
|
+
lines.append(" " + arrow.strip() + " " + remote)
|
|
195
|
+
else:
|
|
196
|
+
lines.append(row.text)
|
|
197
|
+
previous = key
|
|
198
|
+
else:
|
|
199
|
+
lines.append(row.text)
|
|
200
|
+
previous = None
|
|
201
|
+
return lines
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def graph_text(evidence: UnitGraph, *, budget: int | None = None) -> str:
|
|
205
|
+
"""Drop a suffix in reverse priority, accounting for every omitted relation.
|
|
206
|
+
|
|
207
|
+
The notice itself participates in token accounting. Full host files use
|
|
208
|
+
budget=None and the exact same rows. No dataflow nodes are ever rendered.
|
|
209
|
+
"""
|
|
210
|
+
rows = list(evidence.rows)
|
|
211
|
+
suffix: list[Counter] = [Counter() for _ in range(len(rows) + 1)]
|
|
212
|
+
for index in range(len(rows) - 1, -1, -1):
|
|
213
|
+
suffix[index] = suffix[index + 1].copy()
|
|
214
|
+
if rows[index].count:
|
|
215
|
+
suffix[index][rows[index].category] += rows[index].count
|
|
216
|
+
|
|
217
|
+
def render(keep: int) -> str:
|
|
218
|
+
omitted = suffix[keep]
|
|
219
|
+
lines = ["LOCAL GRAPH EVIDENCE (unit ids; dependencies count import names + external call sites; graph evidence is not proof of behavior).",
|
|
220
|
+
f"Batch-internal resolved calls: {evidence.internal_calls} (source visible); "
|
|
221
|
+
f"unmapped resolved calls: {evidence.unmapped_calls}; "
|
|
222
|
+
f"unmapped cfg owners: {evidence.unmapped_cfg}."
|
|
223
|
+
+ ("" if evidence.exception_exits_exported else
|
|
224
|
+
" exception exits are not exported by this graph.")]
|
|
225
|
+
lines += render_rows(rows[:keep])
|
|
226
|
+
lines.append("Omitted: " + (", ".join(f"{k}={v}" for k, v in sorted(omitted.items())) or "none")
|
|
227
|
+
+ ". Omitted evidence is unavailable in this excerpt.")
|
|
228
|
+
return "\n".join(lines)
|
|
229
|
+
|
|
230
|
+
text = render(len(rows))
|
|
231
|
+
if budget is None or count_text_tokens(text) <= budget:
|
|
232
|
+
return text
|
|
233
|
+
low, high = 0, len(rows) - 1
|
|
234
|
+
best = ""
|
|
235
|
+
while low <= high:
|
|
236
|
+
middle = (low + high) // 2
|
|
237
|
+
candidate = render(middle)
|
|
238
|
+
if count_text_tokens(candidate) <= budget:
|
|
239
|
+
best = candidate
|
|
240
|
+
low = middle + 1
|
|
241
|
+
else:
|
|
242
|
+
high = middle - 1
|
|
243
|
+
if best:
|
|
244
|
+
return best
|
|
245
|
+
text = (f"Graph: internal={evidence.internal_calls}, unmapped={evidence.unmapped_calls}, "
|
|
246
|
+
f"unmapped_cfg={evidence.unmapped_cfg}; omitted "
|
|
247
|
+
+ ",".join(f"{k}={v}" for k, v in sorted(suffix[0].items()))
|
|
248
|
+
+ "; dependencies unit=import names + external call sites"
|
|
249
|
+
+ ("" if evidence.exception_exits_exported else
|
|
250
|
+
"; exception exits are not exported by this graph"))
|
|
251
|
+
return text if count_text_tokens(text) <= budget else ""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def graph_prompt_lines(graph: Any, paths: list[str], *, embed: bool,
|
|
255
|
+
inventory: dict[str, dict] | None = None,
|
|
256
|
+
chunks: list[dict] | None = None,
|
|
257
|
+
source_tokens: int | None = DEFAULT_GRAPH_MAX_TOKENS,
|
|
258
|
+
max_tokens: int | None = DEFAULT_GRAPH_MAX_TOKENS,
|
|
259
|
+
graph_file: str | None = None) -> list[str]:
|
|
260
|
+
"""Status notices retain their original meanings; provided evidence is bounded."""
|
|
261
|
+
state, detail = graph_state(graph)
|
|
262
|
+
if state == "absent":
|
|
263
|
+
return ["GRAPH: not provided for this run. Do not infer imports, calls or data flow; state the "
|
|
264
|
+
"boundary in 'uncertainties' when a relation would matter.", ""]
|
|
265
|
+
if state == "unsupported":
|
|
266
|
+
return [f"GRAPH: unsupported for this run ({detail}). Do not infer relations from the graph; "
|
|
267
|
+
"state unproven boundaries in 'uncertainties'.", ""]
|
|
268
|
+
if state == "failed":
|
|
269
|
+
return [f"GRAPH: analysis failed or was unavailable ({detail}). Treat relations as unknown, not "
|
|
270
|
+
"absent, and do not invent them.", ""]
|
|
271
|
+
if state == "empty":
|
|
272
|
+
return ["GRAPH: provided and successfully empty (the analyzer ran and found no node or relation). "
|
|
273
|
+
"An empty graph is a verified result, not a missing graph; still state any unproven boundary.", ""]
|
|
274
|
+
if not any(n.get("path") in set(paths) for n in graph.get("nodes", [])):
|
|
275
|
+
return [f"GRAPH: provided for this run ({detail}) but no graph node anchors the listed file(s). This is "
|
|
276
|
+
"not an empty graph and not a missing graph: the analyzer produced no symbol node for these "
|
|
277
|
+
"paths. Do not infer either the presence or the absence of relations from it; keep unproven "
|
|
278
|
+
"boundaries in 'uncertainties'.", ""]
|
|
279
|
+
evidence = unit_graph(graph, paths, inventory or {}, chunks or [])
|
|
280
|
+
limits = [limit for limit in (max_tokens, source_tokens) if limit is not None]
|
|
281
|
+
budget = max(0, min(limits)) if limits else None
|
|
282
|
+
text = graph_text(evidence, budget=budget)
|
|
283
|
+
if text:
|
|
284
|
+
access = []
|
|
285
|
+
if budget is not None:
|
|
286
|
+
if embed:
|
|
287
|
+
access = ["Graph access: this completion is tool-free; omitted evidence cannot be inspected.", ""]
|
|
288
|
+
elif graph_file:
|
|
289
|
+
access = [f"Graph access: Read the complete evidence for this batch at {graph_file}", ""]
|
|
290
|
+
return access + [text, ""]
|
|
291
|
+
# Like absent/failed graph notices, this is status, not relation payload.
|
|
292
|
+
# A hard zero/tiny payload cap must never imply there were no relations.
|
|
293
|
+
omitted = Counter()
|
|
294
|
+
for row in evidence.rows:
|
|
295
|
+
if row.count:
|
|
296
|
+
omitted[row.category] += row.count
|
|
297
|
+
notice = (f"GRAPH STATUS (outside payload cap): budget={budget} tokens; omitted "
|
|
298
|
+
+ ",".join(f"{kind}={count}" for kind, count in sorted(omitted.items()))
|
|
299
|
+
+ f"; internal={evidence.internal_calls}; unmapped={evidence.unmapped_calls}; "
|
|
300
|
+
+ f"unmapped_cfg={evidence.unmapped_cfg}; dependencies unit=import names + external call sites."
|
|
301
|
+
+ ("" if evidence.exception_exits_exported else
|
|
302
|
+
" exception exits are not exported by this graph."))
|
|
303
|
+
if not embed and graph_file:
|
|
304
|
+
notice += f" Read full evidence: {graph_file}"
|
|
305
|
+
return [notice, ""]
|