AtlatestRepositoryfolio

folio / tree / toolsf0_product_baseline.py

1#!/usr/bin/env python3
2"""Measure the deployed Folio MCP product paths on a disposable estate copy."""
3
4import argparse
5import hashlib
6import json
7import os
8import resource
9import subprocess
10import time
11from pathlib import Path
14def request(proc, payload):
15 started = time.perf_counter_ns()
16 proc.stdin.write(json.dumps(payload, separators=(",", ":")) + "\n")
17 proc.stdin.flush()
18 line = proc.stdout.readline()
19 elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000
20 if not line:
21 raise RuntimeError("Folio closed stdout before replying")
22 return elapsed_ms, json.loads(line)
25def summarize_response(response):
26 text = ""
27 try:
28 text = response["result"]["content"][0]["text"]
29 except (KeyError, IndexError, TypeError):
30 pass
31 return {
32 "id": response.get("id"),
33 "has_result": "result" in response,
34 "has_error": "error" in response,
35 "content_bytes": len(text.encode("utf-8")),
36 "content_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
37 "markers": {
38 "folio_status": "# Folio Status" in text,
39 "total_tasks_1474": "Total tasks: 1474" in text,
40 "rebuilt_2588": "2588 notes indexed" in text,
41 "mutation_added": "Added to inbox:" in text,
42 },
43 }
46def main() -> int:
47 parser = argparse.ArgumentParser()
48 parser.add_argument("--binary", required=True)
49 parser.add_argument("--root", required=True)
50 parser.add_argument("--output", type=Path, required=True)
51 args = parser.parse_args()
52 env = dict(os.environ, FOLIO_ROOT=args.root)
53 before = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
54 wall_start = time.perf_counter_ns()
55 proc = subprocess.Popen(
56 [args.binary], env=env, text=True, stdin=subprocess.PIPE,
57 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
58 )
59 try:
60 startup_ms, initialize = request(proc, {
61 "jsonrpc": "2.0", "id": 1, "method": "initialize",
62 "params": {"protocolVersion": "2025-11-25", "capabilities": {}},
63 })
64 proc.stdin.write('{"jsonrpc":"2.0","method":"notifications/initialized"}\n')
65 proc.stdin.flush()
66 status_ms, status = request(proc, {
67 "jsonrpc": "2.0", "id": 2, "method": "tools/call",
68 "params": {"name": "folio/status", "arguments": {}},
69 })
70 rebuild_ms, rebuild = request(proc, {
71 "jsonrpc": "2.0", "id": 3, "method": "tools/call",
72 "params": {"name": "folio/rebuild-index", "arguments": {}},
73 })
74 mutation_ms, mutation = request(proc, {
75 "jsonrpc": "2.0", "id": 4, "method": "tools/call",
76 "params": {"name": "folio/inbox-add", "arguments": {
77 "text": "F0 disposable product-path mutation control", "tags": "f0-positive"
78 }},
79 })
80 second_status_ms, second_status = request(proc, {
81 "jsonrpc": "2.0", "id": 5, "method": "tools/call",
82 "params": {"name": "folio/status", "arguments": {}},
83 })
84 finally:
85 proc.stdin.close()
86 try:
87 proc.wait(timeout=5)
88 except subprocess.TimeoutExpired:
89 proc.terminate()
90 proc.wait(timeout=5)
91 stderr = proc.stderr.read()
92 after = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
93 report = {
94 "schema": "folio-f0-product-baseline-v1",
95 "binary": str(Path(args.binary).resolve()),
96 "root": str(Path(args.root).resolve()),
97 "exit_code": proc.returncode,
98 "wall_ms": (time.perf_counter_ns() - wall_start) / 1_000_000,
99 "max_rss_kib": max(before, after),
100 "milliseconds": {
101 "cold_start_and_initialize": startup_ms,
102 "common_status_query": status_ms,
103 "full_index_rebuild": rebuild_ms,
104 "controlled_inbox_mutation": mutation_ms,
105 "warm_status_query_after_mutation": second_status_ms,
106 },
107 "controls": {
108 "initialize_response": summarize_response(initialize),
109 "status_response": summarize_response(status),
110 "rebuild_response": summarize_response(rebuild),
111 "mutation_response": summarize_response(mutation),
112 "post_mutation_status_response": summarize_response(second_status),
113 },
114 "stderr": stderr,
115 }
116 args.output.parent.mkdir(parents=True, exist_ok=True)
117 args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
118 return 0 if proc.returncode == 0 else proc.returncode
121if __name__ == "__main__":
122 raise SystemExit(main())