seam-code 0.3.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.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Shared helpers and constants for all MCP tool handlers.
|
|
2
|
+
|
|
3
|
+
This module is a LEAF in the handler dependency tree:
|
|
4
|
+
handler_common → analysis modules, query modules, config
|
|
5
|
+
impact_handler → handler_common
|
|
6
|
+
trace_handler → handler_common
|
|
7
|
+
tools → handler_common, impact_handler, trace_handler
|
|
8
|
+
|
|
9
|
+
Extracted from seam/server/tools.py (Slice 2, P2 #103) as a pure mechanical split.
|
|
10
|
+
No logic change — byte-identical output before and after the extraction.
|
|
11
|
+
|
|
12
|
+
Contains:
|
|
13
|
+
- _HEAVY_FIELDS, _apply_verbosity
|
|
14
|
+
- Handler limit constants (query/search/impact/trace)
|
|
15
|
+
- _serialize_hop, _serialize_edge_hop (used by trace handler + tests)
|
|
16
|
+
- _trace_not_found, _qualified_trace_candidates (used by trace handler)
|
|
17
|
+
- _relativize, _clamp
|
|
18
|
+
- compute_uid, _resolve_uid (P6c stable handle)
|
|
19
|
+
- _invalid_input, _invalid_query
|
|
20
|
+
- _maybe_attach_staleness (P2 staleness banner helper)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import logging
|
|
25
|
+
import sqlite3
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import seam.config as config
|
|
30
|
+
from seam.analysis.flows import EdgeHop, Hop
|
|
31
|
+
from seam.analysis.staleness import StalenessVerdict, _watcher_is_alive, check_staleness
|
|
32
|
+
from seam.query.names import resolve_query_to_defs
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# ── Lean-output: heavy fields stripped when verbose=False ────────────────────
|
|
37
|
+
|
|
38
|
+
# These fields are valuable in verbose mode but inflate every record unnecessarily
|
|
39
|
+
# when the agent only needs the core identity + signature.
|
|
40
|
+
# Keys are ABSENT (not null) in lean mode — lean mode's whole point is fewer bytes.
|
|
41
|
+
# E4: synthesized_by added — provenance detail, stripped in lean mode like resolved_by.
|
|
42
|
+
# kind is NOT here — it is a core field always kept even in lean mode.
|
|
43
|
+
_HEAVY_FIELDS: frozenset[str] = frozenset(
|
|
44
|
+
{
|
|
45
|
+
"decorators",
|
|
46
|
+
"is_exported",
|
|
47
|
+
"visibility",
|
|
48
|
+
"qualified_name",
|
|
49
|
+
"resolved_by",
|
|
50
|
+
"best_candidate",
|
|
51
|
+
"synthesized_by", # E4: provenance detail — stripped in lean, like resolved_by
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _apply_verbosity(record: dict[str, Any], verbose: bool) -> dict[str, Any]:
|
|
57
|
+
"""Strip heavy enrichment fields from a record when verbose=False.
|
|
58
|
+
|
|
59
|
+
Never mutates the input dict.
|
|
60
|
+
|
|
61
|
+
verbose=True → the SAME dict object is returned unchanged (zero-copy fast path;
|
|
62
|
+
callers build records inline and never mutate them post-call, so
|
|
63
|
+
returning the original is safe and byte-identical to pre-Phase-8).
|
|
64
|
+
verbose=False → a NEW dict is returned without the 6 heavy keys (decorators,
|
|
65
|
+
is_exported, visibility, qualified_name, resolved_by,
|
|
66
|
+
best_candidate). signature and all core identity fields are kept.
|
|
67
|
+
"""
|
|
68
|
+
if verbose:
|
|
69
|
+
return record
|
|
70
|
+
# Build a copy without the heavy fields; missing keys are silently skipped.
|
|
71
|
+
return {k: v for k, v in record.items() if k not in _HEAVY_FIELDS}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── Limit bounds (from mcp-tools.yaml) ────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
_QUERY_LIMIT_MIN = 1
|
|
77
|
+
_QUERY_LIMIT_MAX = 50
|
|
78
|
+
_QUERY_LIMIT_DEFAULT = 10
|
|
79
|
+
|
|
80
|
+
_SEARCH_LIMIT_MIN = 1
|
|
81
|
+
_SEARCH_LIMIT_MAX = 100
|
|
82
|
+
_SEARCH_LIMIT_DEFAULT = 20
|
|
83
|
+
|
|
84
|
+
_IMPACT_DEPTH_DEFAULT = 3
|
|
85
|
+
_IMPACT_DIRECTION_DEFAULT = "upstream"
|
|
86
|
+
|
|
87
|
+
_TRACE_DEPTH_MIN = 1
|
|
88
|
+
_TRACE_DEPTH_MAX = 10
|
|
89
|
+
_TRACE_DEPTH_DEFAULT = 10
|
|
90
|
+
# Max bare->qualified candidates tried per trace endpoint on a path miss. Bounds the
|
|
91
|
+
# candidate-pair retry loop to CAP×CAP traces. A bare method name usually resolves to
|
|
92
|
+
# 1–2 qualified symbols; this caps the pathological common-name case ("run", "get").
|
|
93
|
+
_TRACE_ENDPOINT_CAND_CAP = 5
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _serialize_hop(hop: Hop, root: Path) -> dict[str, Any]:
|
|
100
|
+
"""Serialize a flows.Hop dict for JSON output with path relativization.
|
|
101
|
+
|
|
102
|
+
Includes best_candidate (relativized) for AMBIGUOUS hops.
|
|
103
|
+
|
|
104
|
+
E4: when SEAM_EDGE_PROVENANCE=on, adds 'synthesized_by' (the synthesis channel name
|
|
105
|
+
when the hop is heuristic, null when statically extracted). synthesized_by is in
|
|
106
|
+
_HEAVY_FIELDS and therefore stripped in lean mode (verbose=False) by the caller's
|
|
107
|
+
_apply_verbosity call. 'kind' is always present (it was already emitted before E4).
|
|
108
|
+
"""
|
|
109
|
+
raw_candidate: str | None = hop.get("best_candidate")
|
|
110
|
+
record: dict[str, Any] = {
|
|
111
|
+
"from_name": hop["from_name"],
|
|
112
|
+
"to_name": hop["to_name"],
|
|
113
|
+
"kind": hop["kind"],
|
|
114
|
+
"confidence": hop["confidence"],
|
|
115
|
+
"resolved_by": hop.get("resolved_by"),
|
|
116
|
+
"best_candidate": _relativize(raw_candidate, root) if raw_candidate is not None else None,
|
|
117
|
+
}
|
|
118
|
+
# E4: surface synthesized_by when edge-provenance is enabled.
|
|
119
|
+
# null (None) is retained — it is the common "static edge" value and is meaningful.
|
|
120
|
+
if config.SEAM_EDGE_PROVENANCE == "on":
|
|
121
|
+
record["synthesized_by"] = hop.get("synthesized_by")
|
|
122
|
+
return record
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _serialize_edge_hop(hop: EdgeHop, root: Path | None = None) -> dict[str, Any]:
|
|
126
|
+
"""Serialize an EdgeHop TypedDict to a plain dict for JSON-safe output.
|
|
127
|
+
|
|
128
|
+
Phase 5: includes resolved_by for provenance (null when not available).
|
|
129
|
+
Includes best_candidate (relativized) for AMBIGUOUS hops.
|
|
130
|
+
|
|
131
|
+
E4: when SEAM_EDGE_PROVENANCE=on, adds 'synthesized_by' (channel name for
|
|
132
|
+
heuristic edges, null for static). In _HEAVY_FIELDS → stripped in lean mode.
|
|
133
|
+
"""
|
|
134
|
+
raw_candidate = hop.get("best_candidate")
|
|
135
|
+
record: dict[str, Any] = {
|
|
136
|
+
"name": hop["name"],
|
|
137
|
+
"kind": hop["kind"],
|
|
138
|
+
"confidence": hop["confidence"],
|
|
139
|
+
"resolved_by": hop.get("resolved_by"), # Phase 5: null = unknown/fast-path
|
|
140
|
+
# best_candidate for AMBIGUOUS hops; relativized when root is provided.
|
|
141
|
+
"best_candidate": (
|
|
142
|
+
_relativize(raw_candidate, root)
|
|
143
|
+
if (raw_candidate is not None and root is not None)
|
|
144
|
+
else raw_candidate
|
|
145
|
+
),
|
|
146
|
+
}
|
|
147
|
+
# E4: surface synthesized_by when edge-provenance is enabled.
|
|
148
|
+
# null (None) is retained — it is the common "static edge" value and is meaningful.
|
|
149
|
+
if config.SEAM_EDGE_PROVENANCE == "on":
|
|
150
|
+
record["synthesized_by"] = hop.get("synthesized_by")
|
|
151
|
+
return record
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _trace_not_found(source: str, target: str) -> dict[str, Any]:
|
|
155
|
+
"""Empty trace result for an unknown uid/target_uid (P6c).
|
|
156
|
+
|
|
157
|
+
Mirrors the shape trace() produces for a genuinely-not-connected pair so an
|
|
158
|
+
unknown handle reads as "no path", not as an error.
|
|
159
|
+
"""
|
|
160
|
+
return {
|
|
161
|
+
"found": False,
|
|
162
|
+
"source": source,
|
|
163
|
+
"target": target,
|
|
164
|
+
"paths": [],
|
|
165
|
+
"callers_source": [],
|
|
166
|
+
"callees_source": [],
|
|
167
|
+
"callers_target": [],
|
|
168
|
+
"callees_target": [],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _qualified_trace_candidates(conn: sqlite3.Connection, name: str) -> list[str]:
|
|
173
|
+
"""Resolve a BARE trace endpoint to its qualified symbol form(s).
|
|
174
|
+
|
|
175
|
+
WHY: with Tier B receiver inference, method call edges store qualified names
|
|
176
|
+
('Class.method') on both ends, so a bare endpoint matches no edge and trace
|
|
177
|
+
returns nothing. This bridges bare -> qualified (the opposite direction from
|
|
178
|
+
expand_impact_seeds, which bridges qualified -> bare for impact's upstream walk).
|
|
179
|
+
|
|
180
|
+
Returns the qualified symbol names whose last dotted segment equals `name`
|
|
181
|
+
(e.g. 'bar' -> ['Foo.bar']). Returns [] for an already-qualified name (the caller
|
|
182
|
+
has already tried it) or when nothing resolves. Bounded by _TRACE_ENDPOINT_CAND_CAP
|
|
183
|
+
to keep the candidate-pair retry loop small. Never raises.
|
|
184
|
+
"""
|
|
185
|
+
if "." in name:
|
|
186
|
+
return [] # already qualified — caller tried it directly
|
|
187
|
+
try:
|
|
188
|
+
rows = resolve_query_to_defs(conn, name)
|
|
189
|
+
except Exception: # noqa: BLE001 — read path never raises
|
|
190
|
+
logger.debug("_qualified_trace_candidates: resolve failed for %r", name, exc_info=True)
|
|
191
|
+
return []
|
|
192
|
+
out: list[str] = []
|
|
193
|
+
for row in rows:
|
|
194
|
+
qn = row["name"]
|
|
195
|
+
# Keep only the QUALIFIED forms (skip an exact bare match — already attempted).
|
|
196
|
+
if qn != name and qn not in out:
|
|
197
|
+
out.append(qn)
|
|
198
|
+
if len(out) >= _TRACE_ENDPOINT_CAND_CAP:
|
|
199
|
+
break
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _relativize(abs_path: str, root: Path) -> str:
|
|
204
|
+
"""Return abs_path relative to root; falls back to abs_path if not under root."""
|
|
205
|
+
try:
|
|
206
|
+
return str(Path(abs_path).relative_to(root))
|
|
207
|
+
except ValueError:
|
|
208
|
+
return abs_path
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _clamp(value: int, lo: int, hi: int) -> int:
|
|
212
|
+
"""Clamp value to [lo, hi] inclusive."""
|
|
213
|
+
return max(lo, min(hi, value))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ── Stable symbol UID handle (P6c) ────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def compute_uid(file_path: str, start_line: int) -> str:
|
|
220
|
+
"""Compute a stable, opaque handle for a symbol: sha1(file_path)[:8] + ':' + line.
|
|
221
|
+
|
|
222
|
+
P6c: a homonym follow-up otherwise forces an agent to re-disambiguate by file
|
|
223
|
+
path (an extra round-trip). The UID is a pure computed string — NO schema
|
|
224
|
+
change, NO extra DB query. It is surfaced on search/query results and accepted
|
|
225
|
+
as an alternative to `name` on context/impact/trace, where it resolves to the
|
|
226
|
+
EXACT (file, line) symbol, bypassing homonym ambiguity.
|
|
227
|
+
|
|
228
|
+
file_path is the ABSOLUTE path stored in the files table. We hash the absolute
|
|
229
|
+
path (not the relativized output path) so the UID can be resolved back to the
|
|
230
|
+
exact row by recomputing it over each candidate symbol's absolute path.
|
|
231
|
+
"""
|
|
232
|
+
digest = hashlib.sha1(file_path.encode("utf-8")).hexdigest()[:8]
|
|
233
|
+
return f"{digest}:{start_line}"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _resolve_uid(conn: sqlite3.Connection, uid: str) -> tuple[str, str, int] | None:
|
|
237
|
+
"""Resolve a UID handle to the exact (name, abs_file_path, start_line) it pins.
|
|
238
|
+
|
|
239
|
+
Strategy: a UID is sha1(abs_path)[:8] + ':' + start_line. The start_line is
|
|
240
|
+
recoverable directly; the file prefix is NOT reversible, so we narrow by
|
|
241
|
+
start_line in SQL (cheap — uses the line value) and recompute the UID for each
|
|
242
|
+
candidate symbol at that line until one matches. This keeps the read path lean:
|
|
243
|
+
no schema change, no O(files) scan — only the (typically tiny) set of symbols
|
|
244
|
+
that begin at the same line is examined.
|
|
245
|
+
|
|
246
|
+
Returns None for a malformed UID or one that matches no indexed symbol (the
|
|
247
|
+
same not-found contract as an unknown symbol name).
|
|
248
|
+
"""
|
|
249
|
+
prefix, sep, line_str = uid.partition(":")
|
|
250
|
+
if not sep or not line_str.isdigit():
|
|
251
|
+
return None
|
|
252
|
+
start_line = int(line_str)
|
|
253
|
+
|
|
254
|
+
rows = conn.execute(
|
|
255
|
+
"""
|
|
256
|
+
SELECT s.name AS name, f.path AS file
|
|
257
|
+
FROM symbols s
|
|
258
|
+
JOIN files f ON f.id = s.file_id
|
|
259
|
+
WHERE s.start_line = ?
|
|
260
|
+
ORDER BY s.id
|
|
261
|
+
""",
|
|
262
|
+
(start_line,),
|
|
263
|
+
).fetchall()
|
|
264
|
+
|
|
265
|
+
for row in rows:
|
|
266
|
+
if compute_uid(row["file"], start_line) == uid:
|
|
267
|
+
return row["name"], row["file"], start_line
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _invalid_input(message: str) -> dict[str, Any]:
|
|
272
|
+
# Log so an operator can see what an agent actually sent (the error dict
|
|
273
|
+
# otherwise vanishes into the agent's response with no server-side trace).
|
|
274
|
+
logger.warning("rejected (INVALID_INPUT): %s", message)
|
|
275
|
+
return {"error": "INVALID_INPUT", "message": message}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _invalid_query(message: str) -> dict[str, Any]:
|
|
279
|
+
logger.warning("rejected (INVALID_QUERY): %s", message)
|
|
280
|
+
return {"error": "INVALID_QUERY", "message": message}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _maybe_attach_staleness(
|
|
284
|
+
response: dict[str, Any],
|
|
285
|
+
conn: sqlite3.Connection,
|
|
286
|
+
root: Path,
|
|
287
|
+
) -> dict[str, Any]:
|
|
288
|
+
"""Attach the P2 staleness banner to a graph-traversal handler response.
|
|
289
|
+
|
|
290
|
+
Called as the LAST step in the 5 graph-traversal handlers (impact, changes,
|
|
291
|
+
affected, context, trace). Purely additive — never alters existing fields.
|
|
292
|
+
|
|
293
|
+
When SEAM_STALENESS_CHECK=off OR the index is fresh → returns response UNCHANGED
|
|
294
|
+
(byte-identical to pre-feature). When stale → adds a top-level `index_status`
|
|
295
|
+
key: {"stale": True, "reason": str, "hint": str}.
|
|
296
|
+
|
|
297
|
+
WHY last step: staleness is orthogonal to the handler's core logic. Attaching
|
|
298
|
+
it last keeps the core path clean and makes it easy to audit that only an
|
|
299
|
+
additive field is added.
|
|
300
|
+
|
|
301
|
+
Never raises — staleness check degrades gracefully on any IO error.
|
|
302
|
+
"""
|
|
303
|
+
if config.SEAM_STALENESS_CHECK != "on":
|
|
304
|
+
return response
|
|
305
|
+
|
|
306
|
+
# Derive watcher-alive status from the standard PID file location.
|
|
307
|
+
# The convention is root/.seam/watcher.pid (same as main.py start/status).
|
|
308
|
+
pid_file = root / ".seam" / "watcher.pid"
|
|
309
|
+
watcher_alive = _watcher_is_alive(pid_file) is not None
|
|
310
|
+
|
|
311
|
+
verdict: StalenessVerdict = check_staleness(conn, root=root, watcher_alive=watcher_alive)
|
|
312
|
+
if verdict["stale"]:
|
|
313
|
+
# Additive only: build a new dict with index_status at the end.
|
|
314
|
+
# WHY new dict: we never mutate the input (same contract as _apply_verbosity).
|
|
315
|
+
result = dict(response)
|
|
316
|
+
result["index_status"] = {
|
|
317
|
+
"stale": True,
|
|
318
|
+
"reason": verdict["reason"],
|
|
319
|
+
"hint": verdict["hint"],
|
|
320
|
+
}
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
return response
|