This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
Epiphany/tools/epiphany_phase6_context_smoke.py
2026-04-26 05:12:51 +01:00

328 lines
13 KiB
Python

from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
from epiphany_phase5_smoke import AppServerClient
from epiphany_phase5_smoke import DEFAULT_APP_SERVER
from epiphany_phase5_smoke import ROOT
from epiphany_phase5_smoke import require
from epiphany_phase5_smoke import reset_smoke_paths
DEFAULT_CODEX_HOME = ROOT / ".epiphany-smoke" / "phase6-context-codex-home"
DEFAULT_RESULT = ROOT / ".epiphany-smoke" / "phase6-context-smoke-result.json"
DEFAULT_TRANSCRIPT = ROOT / ".epiphany-smoke" / "phase6-context-smoke-transcript.jsonl"
DEFAULT_STDERR = ROOT / ".epiphany-smoke" / "phase6-context-smoke-server.stderr.log"
CONTEXT_CODE_REF = {
"path": "app-server/src/codex_message_processor.rs",
"start_line": 10791,
"end_line": 10989,
"symbol": "map_epiphany_context",
}
def context_patch() -> dict[str, Any]:
return {
"objective": "Expose a read-only Epiphany context shard without returning the full state.",
"activeSubgoalId": "phase6-context-smoke",
"investigationCheckpoint": {
"checkpoint_id": "phase6-context-investigation",
"kind": "source_gathering",
"disposition": "regather_required",
"focus": "Make stale planning obvious when context is re-read after compaction.",
"summary": "Context reflection should expose the full durable checkpoint packet.",
"next_action": "Re-gather source before editing if the packet no longer matches reality.",
"captured_at_turn_id": "turn-phase6-context",
"open_questions": ["How much checkpoint detail belongs in scene versus context?"],
"code_refs": [CONTEXT_CODE_REF],
"evidence_ids": ["ev-context-linked"],
},
"subgoals": [
{
"id": "phase6-context-smoke",
"title": "Live-smoke context shard reflection",
"status": "active",
"summary": "The app-server context surface should return targeted graph/evidence state.",
}
],
"graphs": {
"architecture": {
"nodes": [
{
"id": "context-surface",
"title": "Context shard surface",
"purpose": "Return bounded state context for clients without becoming a writer.",
"code_refs": [CONTEXT_CODE_REF],
}
],
"edges": [
{
"id": "context-edge",
"source_id": "context-surface",
"target_id": "context-surface",
"kind": "reflects",
"code_refs": [CONTEXT_CODE_REF],
}
],
},
"dataflow": {
"nodes": [
{
"id": "typed-state",
"title": "Typed Epiphany state",
"purpose": "Remain authoritative while context shards reflect a slice.",
}
]
},
"links": [
{
"dataflow_node_id": "typed-state",
"architecture_node_id": "context-surface",
"relationship": "bounded-reflection",
}
],
},
"graphFrontier": {
"active_node_ids": ["context-surface"],
"active_edge_ids": ["context-edge"],
},
"graphCheckpoint": {
"checkpoint_id": "phase6-context-smoke",
"graph_revision": 1,
"summary": "Context shard reflection is the active Phase 6 smoke target.",
"frontier_node_ids": ["context-surface"],
},
"evidence": [
{
"id": "ev-context-linked",
"kind": "smoke-test",
"status": "ok",
"summary": "Context smoke linked evidence should come along with the observation.",
"code_refs": [CONTEXT_CODE_REF],
},
{
"id": "ev-context-extra",
"kind": "review",
"status": "ok",
"summary": "Context smoke direct evidence should be returned when requested.",
"code_refs": [CONTEXT_CODE_REF],
},
],
"observations": [
{
"id": "obs-context",
"summary": "Context smoke observation should be selected by id.",
"source_kind": "smoke",
"status": "ok",
"code_refs": [CONTEXT_CODE_REF],
"evidence_ids": ["ev-context-linked"],
}
],
}
def assert_missing_context(response: dict[str, Any]) -> None:
require(response["source"] == "live", "missing context should report live source")
require(response["stateStatus"] == "missing", "missing context should report missing state")
require("stateRevision" not in response, "missing context should not invent a revision")
require(
response["context"]["graph"] == {},
"missing context should not invent graph records",
)
require(
response["missing"]["graphNodeIds"] == ["context-surface"],
"missing context should echo requested missing node ids",
)
def assert_ready_context(response: dict[str, Any]) -> None:
require(response["source"] == "live", "ready context should report live source")
require(response["stateStatus"] == "ready", "ready context should report ready state")
require(response["stateRevision"] == 1, "ready context should preserve state revision identity")
require(
[node["id"] for node in response["context"]["graph"]["architectureNodes"]]
== ["context-surface"],
"context should include active frontier architecture node",
)
require(
[edge["id"] for edge in response["context"]["graph"]["architectureEdges"]]
== ["context-edge"],
"context should include active frontier architecture edge",
)
require(
response["context"]["graph"]["links"][0]["architecture_node_id"] == "context-surface",
"context should include links touching selected graph nodes",
)
require(
response["context"]["frontier"]["active_node_ids"] == ["context-surface"],
"context should include frontier when active frontier is requested by default",
)
require(
response["context"]["checkpoint"]["checkpoint_id"] == "phase6-context-smoke",
"context should expose the current graph checkpoint",
)
require(
response["context"]["investigationCheckpoint"]["checkpoint_id"]
== "phase6-context-investigation",
"context should expose the investigation checkpoint id",
)
require(
response["context"]["investigationCheckpoint"]["disposition"] == "regather_required",
"context should expose the checkpoint disposition",
)
require(
[record["id"] for record in response["context"]["observations"]] == ["obs-context"],
"context should include requested observation",
)
require(
[record["id"] for record in response["context"]["evidence"]]
== ["ev-context-linked", "ev-context-extra"],
"context should include linked and directly requested evidence",
)
require(
response["missing"] == {
"graphNodeIds": ["missing-node"],
"graphEdgeIds": ["missing-edge"],
"observationIds": ["missing-observation"],
"evidenceIds": ["missing-evidence"],
},
"context should report unresolved requested ids",
)
def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
app_server = args.app_server.resolve()
if not app_server.exists():
raise FileNotFoundError(f"codex app-server binary not found: {app_server}")
codex_home = args.codex_home.resolve()
result_path = args.result.resolve()
transcript_path = args.transcript.resolve()
stderr_path = args.stderr.resolve()
reset_smoke_paths(codex_home, result_path, transcript_path, stderr_path)
with AppServerClient(app_server, codex_home, transcript_path, stderr_path) as client:
client.send(
"initialize",
{
"clientInfo": {
"name": "epiphany-phase6-context-smoke",
"title": "Epiphany Phase 6 Context Smoke",
"version": "0.1.0",
},
"capabilities": {"experimentalApi": True},
},
)
client.send("initialized", expect_response=False)
started = client.send(
"thread/start",
{"cwd": str(ROOT / "epiphany-core"), "ephemeral": True},
)
assert started is not None
thread_id = started["thread"]["id"]
missing_notification_start = len(client.notifications)
missing_response = client.send(
"thread/epiphany/context",
{"threadId": thread_id, "graphNodeIds": ["context-surface"]},
)
assert missing_response is not None
require(
missing_response["threadId"] == thread_id,
"context response should echo thread id",
)
assert_missing_context(missing_response)
client.require_no_notification(
"thread/epiphany/stateUpdated",
start_index=missing_notification_start,
)
update_notification_start = len(client.notifications)
update = client.send(
"thread/epiphany/update",
{"threadId": thread_id, "expectedRevision": 0, "patch": context_patch()},
)
assert update is not None
require(update["revision"] == 1, "context smoke patch should advance revision to 1")
client.wait_for_notification(
"thread/epiphany/stateUpdated",
start_index=update_notification_start,
)
context_notification_start = len(client.notifications)
ready_response = client.send(
"thread/epiphany/context",
{
"threadId": thread_id,
"graphNodeIds": ["missing-node"],
"graphEdgeIds": ["missing-edge"],
"observationIds": ["obs-context", "missing-observation"],
"evidenceIds": ["ev-context-extra", "missing-evidence"],
},
)
assert ready_response is not None
assert_ready_context(ready_response)
client.require_no_notification(
"thread/epiphany/stateUpdated",
start_index=context_notification_start,
)
final_read = client.send("thread/read", {"threadId": thread_id, "includeTurns": False})
assert final_read is not None
require(
final_read["thread"]["epiphanyState"]["revision"] == 1,
"context reflection should not mutate state revision",
)
result = {
"threadId": thread_id,
"codexHome": str(codex_home),
"missingStateStatus": missing_response["stateStatus"],
"readyStateStatus": ready_response["stateStatus"],
"readyRevision": ready_response["stateRevision"],
"architectureNodeIds": [
node["id"] for node in ready_response["context"]["graph"]["architectureNodes"]
],
"investigationCheckpointId": ready_response["context"]["investigationCheckpoint"][
"checkpoint_id"
],
"investigationDisposition": ready_response["context"]["investigationCheckpoint"][
"disposition"
],
"evidenceIds": [record["id"] for record in ready_response["context"]["evidence"]],
"contextNotificationCount": client.count_notifications(
"thread/epiphany/stateUpdated",
start_index=context_notification_start,
),
"finalReadRevision": final_read["thread"]["epiphanyState"]["revision"],
}
result_path.parent.mkdir(parents=True, exist_ok=True)
result_path.write_text(
json.dumps(result, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return result
def main() -> int:
parser = argparse.ArgumentParser(
description="Live-smoke the Phase 6 Epiphany context shard surface."
)
parser.add_argument("--app-server", type=Path, default=DEFAULT_APP_SERVER)
parser.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
parser.add_argument("--result", type=Path, default=DEFAULT_RESULT)
parser.add_argument("--transcript", type=Path, default=DEFAULT_TRANSCRIPT)
parser.add_argument("--stderr", type=Path, default=DEFAULT_STDERR)
args = parser.parse_args()
result = run_smoke(args)
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())