fix(export,cli): GraphML null-attr coercion + save-result --answer-file (#1502)

Two cross-platform fixes salvaged from #1502:

- to_graphml: nx.write_graphml raises ValueError on None attribute
  values, so a node/edge carrying a null field crashed the export.
  Coerce None -> "" for node and edge attributes before writing.

- save-result: add --answer-file as an alternative to --answer so long
  or multiline answers can be passed via a file instead of a fragile
  inline shell arg (notably Windows/PowerShell quoting). Exactly one of
  --answer / --answer-file is required.

The rest of #1502 (a version downgrade and a hand-edited generated
skill-windows.md that fails skillgen --check, plus duplicated
windows-scripts) is left for rework on the PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
antonioscarinci
2026-06-29 09:54:08 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 4e4935a64a
commit 407a7f142d
4 changed files with 53 additions and 1 deletions
+6 -1
View File
@@ -2970,13 +2970,18 @@ def main() -> None:
p = _ap.ArgumentParser(prog="graphify save-result")
p.add_argument("--question", required=True)
p.add_argument("--answer", required=True)
p.add_argument("--answer", default=None)
p.add_argument("--answer-file", dest="answer_file", default=None)
p.add_argument("--type", dest="query_type", default="query")
p.add_argument("--nodes", nargs="*", default=[])
p.add_argument("--outcome", choices=("useful", "dead_end", "corrected"), default=None)
p.add_argument("--correction", default=None)
p.add_argument("--memory-dir", default=str(Path(_GRAPHIFY_OUT) / "memory"))
opts = p.parse_args(sys.argv[2:])
if opts.answer_file:
opts.answer = Path(opts.answer_file).read_text(encoding="utf-8").strip()
elif not opts.answer:
p.error("--answer or --answer-file is required")
from graphify.ingest import save_query_result as _sqr
out = _sqr(
+9
View File
@@ -1535,6 +1535,15 @@ def to_graphml(
for _, _, attrs in H.edges(data=True):
for k in [k for k in attrs if k.startswith("_")]:
del attrs[k]
# nx.write_graphml raises ValueError on None attribute values; replace with "".
for node_id in H.nodes():
for key, val in list(H.nodes[node_id].items()):
if val is None:
H.nodes[node_id][key] = ""
for u, v in H.edges():
for key, val in list(H.edges[u, v].items()):
if val is None:
H.edges[u, v][key] = ""
nx.write_graphml(H, output_path)
+17
View File
@@ -82,6 +82,23 @@ def test_to_graphml_has_community_attribute():
content = out.read_text()
assert "community" in content
def test_to_graphml_tolerates_none_attribute_values():
"""nx.write_graphml raises ValueError on a None attribute value; to_graphml
must coerce None -> "" so a node/edge with a null field still exports (#1502)."""
G = make_graph()
communities = cluster(G)
# Inject a None-valued attribute on one node and one edge.
a_node = next(iter(G.nodes()))
G.nodes[a_node]["nullable_field"] = None
if G.number_of_edges():
u, v = next(iter(G.edges()))
G.edges[u, v]["nullable_field"] = None
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "graph.graphml"
to_graphml(G, communities, str(out)) # must not raise
content = out.read_text()
assert "<graphml" in content
def test_to_html_creates_file():
G = make_graph()
communities = cluster(G)
+21
View File
@@ -478,6 +478,27 @@ def test_cli_save_result_rejects_bad_outcome(tmp_path):
assert "great" in (r.stderr + r.stdout)
def test_cli_save_result_reads_answer_from_file(tmp_path):
"""--answer-file lets callers pass a long/multiline answer via a file instead
of a fragile inline arg (Windows/PowerShell quoting), #1502."""
ans = tmp_path / "answer.txt"
ans.write_text("line one\nline two with a \"quote\"\n", encoding="utf-8")
r = _run(["save-result", "--question", "how does auth work?",
"--answer-file", str(ans), "--outcome", "useful"], tmp_path)
assert r.returncode == 0, r.stderr
docs = list((tmp_path / "graphify-out" / "memory").glob("*.md"))
assert docs, "save-result wrote no memory doc"
body = docs[0].read_text(encoding="utf-8")
assert "line one" in body and "line two" in body
def test_cli_save_result_requires_answer_or_answer_file(tmp_path):
"""Neither --answer nor --answer-file -> clean argparse error, not a crash."""
r = _run(["save-result", "--question", "q", "--outcome", "useful"], tmp_path)
assert r.returncode != 0
assert "--answer" in (r.stderr + r.stdout)
def test_cli_reflect_cold_start_writes_empty_lessons(tmp_path):
"""First run with no graphify-out/memory/ still succeeds and writes a valid doc."""
r = _run(["reflect"], tmp_path)