2.1.1 — MCP Phase 0: every high-level op returns an envelope (return-not-print)
Internal refactor per docs/MCP-SERVER-SPEC.md §4; CLI output and exit codes unchanged (wrappers reproduce them from the envelopes): - echo_ops: capture_op/resolve_op/link_op — duplicate gate and offline queueing are data (action: duplicate-gate + candidates; queued: true); the --json stdout-swallow hack is deleted (helper chatter -> stderr inside the cores); capture_op takes body_text= directly (the MCP path, no temp file). - echo_recall.recall_op — one structured result backs prose and --json. - echo_triage.list_op/route_op, echo_reflect.apply_op, echo_session.session_end_op — call capture_op internally, count gate outcomes from envelopes. - echo.scope_show_op/scope_set_op/load_op (+ shared _load_gather), and echo_doctor.run_op (checks as data). New eval/test_ops_api.py asserts the contract for every core: envelope shape AND stdout purity (a stray print would corrupt an MCP response stream). All six suites green. The 2.2 server build now starts directly at the app layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -396,32 +396,10 @@ def _doc_summary(path: str, text: str) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
def _brief(path: str, score: float | None = None, via: str | None = None) -> None:
|
||||
text = links.get_text(path)
|
||||
if text is None:
|
||||
return
|
||||
info = _doc_summary(path, text)
|
||||
head = f"\n### {path}"
|
||||
if score is not None:
|
||||
head += f" (score {score:.2f})"
|
||||
if via:
|
||||
head += f" (via {via})"
|
||||
print(head)
|
||||
stamps = []
|
||||
if info.get("type"):
|
||||
stamps.append(f"type: {info['type']}")
|
||||
if info.get("updated"):
|
||||
stamps.append(f"updated: {info['updated']}")
|
||||
if info.get("status"):
|
||||
stamps.append(f"status: {info['status']}")
|
||||
if stamps:
|
||||
print("_" + " · ".join(stamps) + "_") # staleness is visible, not guessed
|
||||
if info.get("excerpt"):
|
||||
print(info["excerpt"])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- entrypoint
|
||||
def recall(query, limit: int = 8, as_json: bool = False) -> int:
|
||||
def recall_op(query, limit: int = 8) -> dict:
|
||||
"""Core recall (Phase 0): hybrid BM25 + graph, returning the structured envelope
|
||||
({query, primary, linked}) that backs both the CLI renderings and the MCP tool."""
|
||||
q = " ".join(query) if isinstance(query, list) else query
|
||||
today_s = echo.today()
|
||||
index = idx_mod.load()
|
||||
@@ -464,31 +442,49 @@ def recall(query, limit: int = 8, as_json: bool = False) -> int:
|
||||
# --- graph layer ----------------------------------------------------------
|
||||
neighbours = expand_graph(hits, nmap, base, max_hops=MAX_HOPS)
|
||||
|
||||
import echo_output
|
||||
texts = echo.read_many(hits + [p for p, _ in neighbours[: 2 * limit]])
|
||||
primary = []
|
||||
for p in hits:
|
||||
if texts.get(p) is None:
|
||||
continue
|
||||
primary.append({**_doc_summary(p, texts[p]),
|
||||
"score": round(base.get(p, 0.0), 3)})
|
||||
linked = []
|
||||
for p, (sc, via) in neighbours[: 2 * limit]:
|
||||
if texts.get(p) is None:
|
||||
continue
|
||||
linked.append({**_doc_summary(p, texts[p]),
|
||||
"score": round(sc, 3), "via": via})
|
||||
return echo_output.envelope("recall", {"query": q, "primary": primary,
|
||||
"linked": linked})
|
||||
|
||||
|
||||
def _print_hit(info: dict) -> None:
|
||||
"""Prose rendering of one recall hit — same fields the envelope carries."""
|
||||
head = f"\n### {info['path']}"
|
||||
if info.get("score") is not None:
|
||||
head += f" (score {info['score']:.2f})"
|
||||
if info.get("via"):
|
||||
head += f" (via {info['via']})"
|
||||
print(head)
|
||||
stamps = [f"{k}: {info[k]}" for k in ("type", "updated", "status") if info.get(k)]
|
||||
if stamps:
|
||||
print("_" + " · ".join(stamps) + "_") # staleness is visible, not guessed
|
||||
if info.get("excerpt"):
|
||||
print(info["excerpt"])
|
||||
|
||||
|
||||
def recall(query, limit: int = 8, as_json: bool = False) -> int:
|
||||
env = recall_op(query, limit=limit)
|
||||
if as_json:
|
||||
import echo_output
|
||||
texts = echo.read_many(hits + [p for p, _ in neighbours[: 2 * limit]])
|
||||
primary = []
|
||||
for p in hits:
|
||||
if texts.get(p) is None:
|
||||
continue
|
||||
primary.append({**_doc_summary(p, texts[p]),
|
||||
"score": round(base.get(p, 0.0), 3)})
|
||||
linked = []
|
||||
for p, (sc, via) in neighbours[: 2 * limit]:
|
||||
if texts.get(p) is None:
|
||||
continue
|
||||
linked.append({**_doc_summary(p, texts[p]),
|
||||
"score": round(sc, 3), "via": via})
|
||||
env = echo_output.envelope("recall", {"query": q, "primary": primary,
|
||||
"linked": linked})
|
||||
print(json.dumps(env, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
print(f"== recall: {q} ==")
|
||||
print(f"\n# Primary hits ({len(hits)})")
|
||||
for p in hits:
|
||||
_brief(p, score=base.get(p))
|
||||
print(f"\n# Linked context ({len(neighbours)})")
|
||||
for p, (sc, via) in neighbours[: 2 * limit]:
|
||||
_brief(p, score=sc, via=via)
|
||||
print(f"== recall: {env['query']} ==")
|
||||
print(f"\n# Primary hits ({len(env['primary'])})")
|
||||
for info in env["primary"]:
|
||||
_print_hit(info)
|
||||
print(f"\n# Linked context ({len(env['linked'])})")
|
||||
for info in env["linked"]:
|
||||
_print_hit(info)
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user