From 8dc5aff08e877485a61545324af59ff888f5c5fe Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:40:39 +0800 Subject: [PATCH 01/23] feat: expose allowlisted local tools through JSON CLI and deterministic agent plan --- tool_cli.py | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 tool_cli.py diff --git a/tool_cli.py b/tool_cli.py new file mode 100644 index 0000000..a30f330 --- /dev/null +++ b/tool_cli.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""Local Tool / CLI / agent-plan adapter for the repository's existing core. + +This is a bounded JSON invocation protocol, NOT an MCP transport/server. +It does not invoke a language model or run arbitrary shell commands. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import sys +import tempfile +from pathlib import Path +from typing import Any + +REPOSITORY = "sci-render-kit" +PROFILE = REPOSITORY + "/tool-execution" +TOOL_DEFINITIONS = [ + { + "name": "figure.preflight", + "description": "Run the canonical schema, runtime visual and claim-communication checks without rendering.", + "inputSchema": { + "type": "object", + "properties": { + "recipe_path": { + "type": "string" + }, + "profile_name": { + "type": "string" + } + }, + "required": [ + "recipe_path" + ], + "additionalProperties": false + } + }, + { + "name": "communication.transfer", + "description": "In-memory bounded figure-communication handoff from existing figure-evidence sidecar.", + "inputSchema": { + "type": "object", + "properties": { + "figure_evidence_path": { + "type": "string" + }, + "destination": { + "type": "string" + }, + "purpose": { + "type": "string" + } + }, + "required": [ + "figure_evidence_path" + ], + "additionalProperties": false + } + } +] + + +def _digest(value: Any) -> str: + raw = json.dumps(value, sort_keys=True, ensure_ascii=False, + separators=(",", ":"), default=str).encode("utf-8") + return "sha256:" + hashlib.sha256(raw).hexdigest() + + +def _validate_args(name: str, args: Any) -> None: + spec = next((item for item in TOOL_DEFINITIONS if item["name"] == name), None) + if spec is None: + raise ValueError(f"tool not allowlisted: {name}") + if not isinstance(args, dict): + raise TypeError("tool arguments must be a JSON object") + schema = spec["inputSchema"] + unknown = set(args) - set(schema["properties"]) + if unknown: + raise ValueError(f"unknown tool arguments: {sorted(unknown)}") + missing = [key for key in schema["required"] if key not in args] + if missing: + raise ValueError(f"missing required arguments: {missing}") + types = {"string": str, "boolean": bool, "array": list, "object": dict} + for key, value in args.items(): + kind = schema["properties"][key]["type"] + if not isinstance(value, types[kind]): + raise TypeError(f"{key} must be {kind}") + if kind == "string" and not value.strip(): + raise ValueError(f"{key} must be nonempty") + + +def _dispatch(name: str, args: dict) -> dict: + + if name == "figure.preflight": + import yaml + from jsonschema import validate + from sci_render import load_yaml, evaluate_pre_render_rules + from core.claim_binding_audit import audit_claim_communication + recipe = load_yaml(args["recipe_path"]) + validate(instance=recipe, schema=load_yaml("metadata/recipe.schema.yaml")) + profile_name = args.get("profile_name", "nature") + if "/" in profile_name or "\\" in profile_name or profile_name.startswith("."): + raise ValueError("profile_name must be a registered local profile basename") + profile = load_yaml(f"profiles/{profile_name}.yaml") + checks = evaluate_pre_render_rules(recipe, profile, load_yaml("quality/rules.yaml")) + checks.extend(audit_claim_communication(recipe)) + counts = {key: sum(item.get("severity") == key for item in checks) + for key in ("error", "warning", "info")} + return { + "profile": "sci-render-kit/preflight", + "recipe_id": recipe["id"], + "target_profile": profile_name, + "evaluation_status": "failed" if counts["error"] else ( + "observations" if counts["warning"] or counts["info"] else "clean"), + "counts": counts, + "findings": checks, + "figure_generated": False, + "scientific_validity_claim": False, + "publisher_acceptance_claim": False, + "accessibility_conformance_claim": False, + } + if name == "communication.transfer": + from core.communication_transfer import build_communication_transfer + return build_communication_transfer( + args["figure_evidence_path"], + destination=args.get("destination"), + purpose=args.get("purpose"), + ) + raise ValueError(f"tool not allowlisted: {name}") + + +def invoke(name: str, args: Any) -> dict: + """One typed receipt: local invocation result, not a truth verdict.""" + receipt = { + "profile": PROFILE, + "repository": REPOSITORY, + "tool": name, + "input_sha256": _digest(args), + "status": "failed", + "assertion_basis": "local-tool-invocation", + "scientific_validity_claim": False, + "independent_reproduction_claim": False, + } + try: + _validate_args(name, args) + receipt["output"] = _dispatch(name, args) + receipt["status"] = "success" + except (OSError, ValueError, TypeError, KeyError, ImportError) as exc: + receipt["error"] = {"type": type(exc).__name__, "message": str(exc)} + return receipt + + +def run_plan(plan: Any) -> dict: + """Bounded deterministic coordinator; no model or implicit approval.""" + if not isinstance(plan, dict) or set(plan) != {"steps"}: + raise ValueError("plan must contain only a steps array") + steps = plan["steps"] + if not isinstance(steps, list) or not 1 <= len(steps) <= 32: + raise ValueError("plan must contain between 1 and 32 steps") + ids: set[str] = set() + results = [] + for index, step in enumerate(steps): + if not isinstance(step, dict) or set(step) != {"id", "tool", "arguments"}: + raise ValueError(f"step[{index}] requires id, tool and arguments") + step_id = step["id"] + if not isinstance(step_id, str) or not step_id or step_id in ids: + raise ValueError(f"step[{index}] has blank or duplicate id") + if not isinstance(step["tool"], str): + raise ValueError(f"step[{index}] tool must be a string") + ids.add(step_id) + receipt = invoke(step["tool"], step["arguments"]) + results.append({"id": step_id, "receipt": receipt}) + if receipt["status"] != "success": + break + return { + "profile": REPOSITORY + "/agent-plan", + "repository": REPOSITORY, + "plan_sha256": _digest(plan), + "status": "failed" if results[-1]["receipt"]["status"] != "success" else "success", + "completed_steps": len(results), + "requested_steps": len(steps), + "steps": results, + "execution_mode": "deterministic-declared-plan-no-model", + "scientific_validity_claim": False, + } + + +def _read_input(path: str) -> Any: + text = sys.stdin.read() if path == "-" else Path(path).read_text(encoding="utf-8") + return json.loads(text) + + +def _write_atomic(path: str, content: str) -> None: + target = Path(path) + target.parent.mkdir(parents=True, exist_ok=True) + fd, temporary = tempfile.mkstemp(dir=str(target.parent), prefix="." + target.name + ".") + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(content) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, target) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Local bounded Tool/CLI/agent-plan interface") + parser.add_argument("command", choices=("list", "call", "agent")) + parser.add_argument("tool", nargs="?", help="allowlisted tool for call") + parser.add_argument("--input", default="-", help="JSON file or stdin (-)") + parser.add_argument("--output", help="optional explicit atomic JSON receipt output") + args = parser.parse_args(argv) + try: + if args.command == "list": + if args.tool: + raise ValueError("list takes no tool argument") + result = {"profile": REPOSITORY + "/tool-catalog", "tools": TOOL_DEFINITIONS} + elif args.command == "call": + if not args.tool: + raise ValueError("call requires a tool name") + result = invoke(args.tool, _read_input(args.input)) + else: + if args.tool: + raise ValueError("agent takes no tool argument") + result = run_plan(_read_input(args.input)) + except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc: + result = {"profile": PROFILE, "status": "failed", + "error": {"type": type(exc).__name__, "message": str(exc)}} + rendered = json.dumps(result, indent=2, ensure_ascii=False, default=str) + "\n" + if args.output: + _write_atomic(args.output, rendered) + sys.stdout.write(rendered) + return 1 if result.get("status") == "failed" else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 3c8bd6d7b94d911571bf522ba14839c2eda68d4d Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:42:51 +0800 Subject: [PATCH 02/23] feat(ui): local-first evidence console for tool and agent receipts --- ui/tri_repo_console.html | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 ui/tri_repo_console.html diff --git a/ui/tri_repo_console.html b/ui/tri_repo_console.html new file mode 100644 index 0000000..e0845a2 --- /dev/null +++ b/ui/tri_repo_console.html @@ -0,0 +1,85 @@ + + + + + + +Research Operations · Evidence Console + + + +
+ +
+
Local-first · Evidence Console

工具执行,证据可见

读取三科研仓的 Tool / CLI / Agent JSON 收据,查看状态、输入摘要和逐步结果。文件仅在当前浏览器中处理,不连接 GitHub、不执行工具、不上传数据

OFFLINE VIEWER
+
导入执行收据

支持 * /tool-execution、* /agent-plan,以及工具目录 JSON · 单文件最大 2 MB

+
+
仓库未加载
+
执行状态—
+
已执行步骤—
+
证据来源本地文件
+
+

执行轨迹

尚未读取 JSON
选择工具收据后显示执行步骤与结果
+

原始证据

只读 · 不修改
展开 JSON / 审阅全部字段
没有导入记录
+ +
+ + + From cda780ccf084d6bc861ddb96a00df441463c6ff8 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:44:08 +0800 Subject: [PATCH 03/23] test: verify allowlisted Tool CLI and deterministic agent receipts --- tests/test_tool_cli.py | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/test_tool_cli.py diff --git a/tests/test_tool_cli.py b/tests/test_tool_cli.py new file mode 100644 index 0000000..1734503 --- /dev/null +++ b/tests/test_tool_cli.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Native Tool/CLI/agent-plan contract tests; not a scientific validation.""" +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import tool_cli + +ROOT = Path(__file__).resolve().parents[1] + + +class ToolInterfaceTests(unittest.TestCase): + def test_catalog_exposes_only_allowlisted_schemas(self): + self.assertEqual(len(tool_cli.TOOL_DEFINITIONS), 2) + self.assertTrue(all(tool["inputSchema"]["type"] == "object" + for tool in tool_cli.TOOL_DEFINITIONS)) + + def test_unknown_tool_fails_closed(self): + response = tool_cli.invoke("unregistered.remote_exec", {}) + self.assertEqual(response["status"], "failed") + self.assertEqual(response["error"]["type"], "ValueError") + self.assertFalse(response["scientific_validity_claim"]) + + def test_invalid_argument_shape_fails_closed(self): + tool = tool_cli.TOOL_DEFINITIONS[0]["name"] + self.assertEqual(tool_cli.invoke(tool, [])["status"], "failed") + self.assertEqual(tool_cli.invoke(tool, {"unexpected": "yes"})["status"], "failed") + + def test_agent_plan_stops_on_first_failure(self): + result = tool_cli.run_plan({"steps": [ + {"id": "bad", "tool": "unknown", "arguments": {}}, + {"id": "not-executed", "tool": "unknown", "arguments": {}}, + ]}) + self.assertEqual(result["status"], "failed") + self.assertEqual(result["completed_steps"], 1) + self.assertEqual(result["requested_steps"], 2) + self.assertEqual(result["steps"][0]["id"], "bad") + + def test_agent_rejects_duplicate_ids_and_unbounded_plans(self): + with self.assertRaises(ValueError): + tool_cli.run_plan({"steps": [ + {"id": "x", "tool": "x", "arguments": {}}, + {"id": "x", "tool": "x", "arguments": {}}, + ]}) + with self.assertRaises(ValueError): + tool_cli.run_plan({"steps": []}) + with self.assertRaises(ValueError): + tool_cli.run_plan({"steps": [ + {"id": str(i), "tool": "unknown", "arguments": {}} for i in range(33) + ]}) + + def test_cli_list_and_atomic_receipt(self): + with tempfile.TemporaryDirectory() as root: + output = Path(root) / "receipt.json" + listed = subprocess.run( + [sys.executable, "tool_cli.py", "list", "--output", str(output)], + cwd=ROOT, capture_output=True, text=True, check=True, + ) + self.assertEqual(json.loads(listed.stdout)["profile"], + tool_cli.REPOSITORY + "/tool-catalog") + self.assertEqual(json.loads(output.read_text())["tools"], + tool_cli.TOOL_DEFINITIONS) + + def test_real_preflight_on_bundled_example(self): + receipt = tool_cli.invoke("figure.preflight", { + "recipe_path": "examples/line_chart.yaml", + "profile_name": "nature", + }) + self.assertEqual(receipt["status"], "success", receipt) + self.assertEqual(receipt["output"]["recipe_id"], "line-chart-example") + self.assertEqual(receipt["output"]["counts"]["error"], 0, receipt) + self.assertFalse(receipt["output"]["figure_generated"]) + self.assertFalse(receipt["output"]["scientific_validity_claim"]) + + def test_profile_path_escape_rejected(self): + receipt = tool_cli.invoke("figure.preflight", { + "recipe_path": "examples/line_chart.yaml", + "profile_name": "../outside", + }) + self.assertEqual(receipt["status"], "failed") + self.assertIn("profile_name", receipt["error"]["message"]) + + + +if __name__ == "__main__": + unittest.main() From c94d5f7d96aadb9a87b02f847b904e834973cf65 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:44:41 +0800 Subject: [PATCH 04/23] fix(examples): add missing schema-aligned baseline figure recipe --- examples/line_chart.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 examples/line_chart.yaml diff --git a/examples/line_chart.yaml b/examples/line_chart.yaml new file mode 100644 index 0000000..f6b0d61 --- /dev/null +++ b/examples/line_chart.yaml @@ -0,0 +1,24 @@ +# Bounded synthetic fixture for CLI demonstration; not scientific source data. +id: line-chart-example +name: Synthetic two-series tool/CLI fixture +type: line-chart +data: + control: [1, 2, 3, 4] + treatment: [2, 3, 4, 5] +aesthetics: + title: Synthetic observations (fixture) + x_label: Sample + y_label: Value (arbitrary units) + palette: ["#0072B2", "#D55E00"] +accessibility: + require_alt_text: true + alt_text: "Synthetic example: control rises from 1 to 4 and treatment from 2 to 5." + redundant_encoding: required +process_disclosure: + ai_assistance: not_declared + human_review: not_declared +output: + dir: output + filename: line_chart.png + format: png +notes: "Illustrative numbers only; no measured experiment, model claim, or publisher acceptance." From 7dcde554fcdbc8f2beb3144a992d40c97a1a6e9d Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:46:59 +0800 Subject: [PATCH 05/23] docs: define tool/CLI/agent invocation and evidence boundary --- .../TOOL_CLI_AGENT_CONTRACT.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/02-examples-and-contracts/TOOL_CLI_AGENT_CONTRACT.md diff --git a/docs/02-examples-and-contracts/TOOL_CLI_AGENT_CONTRACT.md b/docs/02-examples-and-contracts/TOOL_CLI_AGENT_CONTRACT.md new file mode 100644 index 0000000..62e15aa --- /dev/null +++ b/docs/02-examples-and-contracts/TOOL_CLI_AGENT_CONTRACT.md @@ -0,0 +1,61 @@ +# Native Tool / CLI / Agent Contract — 2026-10-19 + +本仓 Tool CLI 是对已有 canonical core 模块的显式封装,不是第二套算法、独立运行时、MCP Server 或 LLM 自动 Agent + +## Invocation contract + +Tool discovery: python3 tool_cli.py list + +Single call: python3 tool_cli.py call TOOL --input request.json --output output/tool-receipt.json + +Plan runner: python3 tool_cli.py agent --input examples/tool_plan.json --output output/agent-plan.json + +--input 默认为 stdin (-),--output 为可选的显式原子文件写入。未指定 --output 时只向 stdout 打印 JSON;本接口不会主动远程请求、运行 shell 或调用 LLM + +### Envelope + +单工具输出 profile = REPOSITORY/tool-execution,包含 repository、tool、status、input_sha256、assertion_basis、output 或 error。input_sha256 是排序键、紧凑序列化后的参数 SHA-256,属于输入身份,不证明内容正确 + +Agent 输出 profile = REPOSITORY/agent-plan,包含 plan_sha256、status、completed_steps、requested_steps、steps 和 execution_mode。Plan 输入为带 steps 数组的 JSON 对象,元素包含唯一 id、tool、arguments,允许 1–32 步,失败时停止并保留已经执行的步骤,后续步骤保持未执行而非虚报完成 + +Tools 白名单定义在 tool_cli.py 中,未知工具、错误类型、缺失必需参数、重复 step ID 均为显式错误。exit 0 代表调用正常结束,不代表输出业务 status 或 scientific validity 通过。工具结果中的 valid、evaluation_status、audit_state 由原生模块单独解释 + +## Tool map + +figure.preflight — required recipe_path, optional profile_name; delegates to current YAML schema, sci_render.evaluate_pre_render_rules and core.claim_binding_audit. Does NOT render a figure. evaluation_status:failed can appear inside a successfully executed tool receipt + +communication.transfer — required figure_evidence_path, optional destination/purpose; delegates to core.communication_transfer.build_communication_transfer, with non-inheritance constraints + +## Human / Agent / UI boundary + +Human 或外部 Agent -> JSON parameters / plan -> local allowlisted Tool CLI -> canonical core -> tool/agent receipt -> sci-render-kit offline Evidence Console + +- 这不是 MCP transport。不能只因使用 inputSchema / JSON 或工具清单,就宣称支持某个 MCP revision。需要正式 MCP 时应另行使用经核验的 SDK +- 这是确定性声明式 Agent 计划执行,不会执行模型生成的任意代码,不具备自主推理、持久调度或用户权限认证 +- 本地进程能访问传入的文件路径,不具备 OS 级沙箱和网络隔离保证,不能直接执行不可信计划 +- GUI 位于 sci-render-kit/ui/tri_repo_console.html,本地浏览器手动选择 JSON,不向服务器上传,也不验证 GitHub / CI 实时状态 +- 科学有效性、独立复现和 publisher acceptance 不会从工具成功或绿色 UI 徽标中推导出来 +- 测试入口:python3 -m unittest tests.test_tool_cli -v;仓库 make test 仍属人工可选检查,而非合并门禁 + +## Demo + +Run from repository root: + + python3 tool_cli.py list + echo '{"recipe_path":"examples/line_chart.yaml","profile_name":"nature"}' | python3 tool_cli.py call figure.preflight + python3 tool_cli.py agent --input examples/tool_plan.json --output output/agent-plan.json + python3 sci_render.py examples/line_chart.yaml --profile nature --backend matplotlib + +Open ui/tri_repo_console.html, select an exported receipt JSON, inspect step status and original bytes as JSON text. UI never invokes the renderer + +## Evidential constraints + +operation result != scientific truth + +claim transfer != acceptance + +figure preflight != generated figure + +UI rendering != current repository state + +Draft PR != merged or CI validated From 861650dac919ba601a000a5d1509e2da876bd481 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:47:36 +0800 Subject: [PATCH 06/23] examples: add bounded executable agent plan for canonical tools --- examples/tool_plan.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 examples/tool_plan.json diff --git a/examples/tool_plan.json b/examples/tool_plan.json new file mode 100644 index 0000000..e115194 --- /dev/null +++ b/examples/tool_plan.json @@ -0,0 +1,12 @@ +{ + "steps": [ + { + "id": "figure-preflight", + "tool": "figure.preflight", + "arguments": { + "recipe_path": "examples/line_chart.yaml", + "profile_name": "nature" + } + } + ] +} From 4d0ba1554060993f6fa420b17e58599e818d8eb3 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:48:34 +0800 Subject: [PATCH 07/23] docs(ui): specify offline evidence console visual tokens and safety behavior --- .../TOOL_UI_STYLE.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/02-examples-and-contracts/TOOL_UI_STYLE.md diff --git a/docs/02-examples-and-contracts/TOOL_UI_STYLE.md b/docs/02-examples-and-contracts/TOOL_UI_STYLE.md new file mode 100644 index 0000000..774ca54 --- /dev/null +++ b/docs/02-examples-and-contracts/TOOL_UI_STYLE.md @@ -0,0 +1,48 @@ +# Three-Repository Evidence Console — UI Style & Interaction Contract + +**Owner:** sci-render-kit communication/presentation plane, 2026-10-19 +**Artifact:** [ui/tri_repo_console.html](../../ui/tri_repo_console.html) + +## Purpose + +Offline, local-first viewer for JSON receipts produced by each repository's tool_cli.py. It is a human review surface, not an execution engine, provider terminal, live GitHub dashboard, automatic scientific evaluator, or WCAG certification + +## Design language + +Visual direction: calm research operations console — dark navy workbench, restrained mint state highlights, high density with generous card padding, explicit state text. No gradient-as-evidence, no scores fabricated from source data + +| Token | Hex | Use | +|---|---|---| +| bg | #09111d | page background | +| panel | #111e2d | evidence surfaces | +| panel2 | #162537 | local distinction | +| line | #293d51 | neutral boundaries | +| fg | #e8f0f6 | primary reading text | +| muted | #9db1c5 | descriptive metadata | +| accent | #62ddc1 | selected state/action | +| warn | #ffd18a | warnings (only with text) | +| danger | #ffa39c | failure (only with text) | + +Typography: operating-system sans stack for body, monospaced pre for exact JSON; heading 30–47 px responsive, body 15 px. Layout: 235 px static sidebar at wide widths; single-column responsive mobile; summary cards wrap at 900 / 500 px + +## Observable interaction + +1. User selects or drags one local JSON file, limited to 2 MiB +2. Browser parses JSON locally using File.text; page issues no fetch or storage calls +3. Profile whitelist: /tool-execution, /agent-plan, /tool-catalog +4. Text nodes use textContent rather than HTML injection; no dynamic innerHTML with user JSON +5. Show repository, execution status, actual requested vs completed steps, per-step tool status and input digest; raw JSON can be expanded for review +6. Unknown and failure status remain textual and are not guessed from badge colors + +## Accessibility and boundaries + +- File chooser, focus-visible buttons, keyboard Enter/Space upload zone, semantic summary and live region +- No screenshots or color contrast tests in this PR demonstrate WCAG compliance +- Green success means a local tool returned a success envelope, NOT source truth, CI success or independent reproduction +- Importing a receipt is not executing its tool or approving a write +- Current repository/CI state is not fetched or represented; display is point-in-time evidence from the imported file +- The historical experimental core/observer_dashboard.py remains untouched and unintegrated; name similarity is not evidence of runtime support + +## Next verification gate + +Test visually with desktop/mobile viewport, keyboard-only navigation, screen reader if available, and malicious JSON containing angle brackets or script-looking strings. Review both pass and fail snapshots. Browser-based visual verification is **NOT_EXECUTED** unless a matching run report is attached to the change From 6f85527b096ef653d950f20cc55ae22713c5dd41 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:51:15 +0800 Subject: [PATCH 08/23] integrate Tool/CLI/agent and offline UI in current repository authority --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 2dc52e5..6165214 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ [Architecture](docs/01-source-and-explanation/ARCHITECTURE.md) · [Research Contract](docs/02-examples-and-contracts/RESEARCH_CONTRACT.md) · [Figure Claim Contract](docs/02-examples-and-contracts/FIGURE_CLAIM_CONTRACT.md) · [Communication Transfer](docs/02-examples-and-contracts/COMMUNICATION_TRANSFER_CONTRACT.md) · [Assertion Basis & Communication Coverage](docs/02-examples-and-contracts/ASSERTION_BASIS_AND_COMMUNICATION_COVERAGE.md) · [Maintenance](docs/03-maintenance-and-audit/MAINTENANCE_CADENCE.md) · [Document Status](docs/03-maintenance-and-audit/DOCUMENT_STATUS.md) · [August Stage Close](docs/03-maintenance-and-audit/history/STAGE_2026_08_MAINTENANCE.md) · [Frontier Alignment](docs/03-maintenance-and-audit/history/FRONTIER_ALIGNMENT.md) · [Examples](examples/README.md) +## Native Tool / CLI / Agent interface (2026-10-19 integration) + +The repository exposes `tool_cli.py` for allowlisted local JSON tool discovery, calls, and deterministic declared agent plans: `python3 tool_cli.py list`, `python3 tool_cli.py call figure.preflight --input request.json`, or `python3 tool_cli.py agent --input examples/tool_plan.json --output output/agent-plan.json`. + +[Tool/CLI/Agent contract](docs/02-examples-and-contracts/TOOL_CLI_AGENT_CONTRACT.md) · [Offline Evidence Console](ui/tri_repo_console.html) · [UI style/interaction contract](docs/02-examples-and-contracts/TOOL_UI_STYLE.md). The Console only visualizes locally selected receipts; neither UI nor preflight is a render engine, MCP server, CI monitor, independent reproduction or publisher acceptance claim. + ## Positioning `sci-render-kit` does not decide whether a scientific conclusion is correct. It compiles a declarative recipe into a supported visual artifact while keeping communication semantics inspectable and portable From 66b8b1b4c9f029fe175443b2e90ba72523c0840e Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:51:18 +0800 Subject: [PATCH 09/23] integrate Tool/CLI/agent and offline UI in current repository authority --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f189655..e2617f1 100644 --- a/Makefile +++ b/Makefile @@ -12,10 +12,14 @@ clean: rm -rf __pycache__ .pytest_cache output/* @echo "Cleaned local caches and generated artifacts" -test: test-core test-accessibility +test: test-core test-accessibility test-tool-interface test-core: python3 tests/test_all.py test-accessibility: python3 -m unittest tests.test_accessibility -v + +.PHONY: test-tool-interface +test-tool-interface: + python3 -m unittest tests.test_tool_cli -v From d9553bbc98dc5c0c3bde0e5245e4d0fb180460c4 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:51:20 +0800 Subject: [PATCH 10/23] integrate Tool/CLI/agent and offline UI in current repository authority --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c210a06..fb8c79b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -261,3 +261,7 @@ Do not auto-merge, force-push, or write maintenance repairs directly to `main`. ## Public boundary Do not publish private Jules prompts, repository memory, hidden reasoning, credentials, or unrelated operator context. Public governance may preserve the effect of a rule without copying private control text. + +## Native Tool / CLI / Agent and UI interface (2026-10-19 proposal) + +`tool_cli.py` exposes local preflight/communication handoff as two allowlisted tools with typed receipts; it does not invoke actual rendering or a model. Declared agent plans stop after failure. The offline `ui/tri_repo_console.html` uses locally selected JSON text only and is not the experimental `core/observer_dashboard.py` or a live observer. See `docs/02-examples-and-contracts/TOOL_CLI_AGENT_CONTRACT.md` and `TOOL_UI_STYLE.md`; do not infer MCP, scientific validity, full WCAG certification, publisher acceptance or completed CI from this proposal. From 381fd1036e6638ed024401d701f233db55df0488 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:51:22 +0800 Subject: [PATCH 11/23] integrate Tool/CLI/agent and offline UI in current repository authority --- MANIFEST.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/MANIFEST.yaml b/MANIFEST.yaml index 4290e94..b562093 100644 --- a/MANIFEST.yaml +++ b/MANIFEST.yaml @@ -441,3 +441,24 @@ scientific_boundaries: # no A→H synthesis artifact is inferred or manufactured by this annotation. # This is routing metadata only: calibrated=2026-09-22 is preserved, and no runtime capability, # machine-contract semantics, scientific validation, reproduction status, or release authority is changed. + +# Native Tool/CLI and local UI proposal, 2026-10-19 +native_tool_cli: + module: tool_cli.py + profile: sci-render-kit/tool-execution + agent_plan_profile: sci-render-kit/agent-plan + declared_plan_max_steps: 32 + allowlisted_tool_count: 2 + output_receipt: explicit_atomic_write_only + transport: local_json_cli_not_mcp_server + model_calls: false + remote_tool_execution: false + scientific_validity_claim: false + independent_reproduction_claim: false + proof_scope: local_proposal_requires_real_test_evidence +native_evidence_ui: + path: ui/tri_repo_console.html + mode: offline_local_json_file_viewer + network_execution: false + live_provider_or_ci_data: false + wcag_conformance_claim: false From edd23340bb74c3704d54037c2a49b4d1c42721c5 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:51:27 +0800 Subject: [PATCH 12/23] integrate Tool/CLI/agent and offline UI in current repository authority --- docs/02-examples-and-contracts/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/02-examples-and-contracts/README.md b/docs/02-examples-and-contracts/README.md index 1b4737d..8a3f5b7 100644 --- a/docs/02-examples-and-contracts/README.md +++ b/docs/02-examples-and-contracts/README.md @@ -104,3 +104,8 @@ For a new figure/communication integration: 6. keep scientific claim support and publication/accessibility status separately sourced. Architecture and implementation explanation live under [`../01-source-and-explanation/`](../01-source-and-explanation/). Maintenance/audit material in class 03 is operational/historical context, not scientific validity. + +## Native Tool / CLI / Agent and UI interface (2026-10-19) + +- [TOOL_CLI_AGENT_CONTRACT.md](TOOL_CLI_AGENT_CONTRACT.md) — allowlisted tools, JSON receipts, declared-plan runner and validation limits +- [TOOL_UI_STYLE.md](TOOL_UI_STYLE.md) — offline tri-repository evidence console style, interaction and status boundaries From 0fbbd28257f9bb012f8007ffb2bb26ff9f35126f Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:53:38 +0800 Subject: [PATCH 13/23] fix: preserve structured receipt for schema and unexpected core failures --- tool_cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tool_cli.py b/tool_cli.py index a30f330..9997a06 100644 --- a/tool_cli.py +++ b/tool_cli.py @@ -146,7 +146,7 @@ def invoke(name: str, args: Any) -> dict: _validate_args(name, args) receipt["output"] = _dispatch(name, args) receipt["status"] = "success" - except (OSError, ValueError, TypeError, KeyError, ImportError) as exc: + except Exception as exc: # Core/schema failures remain structured tool errors. receipt["error"] = {"type": type(exc).__name__, "message": str(exc)} return receipt @@ -226,7 +226,7 @@ def main(argv: list[str] | None = None) -> int: if args.tool: raise ValueError("agent takes no tool argument") result = run_plan(_read_input(args.input)) - except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc: + except Exception as exc: # Malformed JSON/plan remains a structured CLI failure. result = {"profile": PROFILE, "status": "failed", "error": {"type": type(exc).__name__, "message": str(exc)}} rendered = json.dumps(result, indent=2, ensure_ascii=False, default=str) + "\n" From 58d2ca671d159f828d6dbe4197578b33cb4073c0 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:54:10 +0800 Subject: [PATCH 14/23] fix: report explicit receipt-write failures as structured CLI errors --- tool_cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tool_cli.py b/tool_cli.py index 9997a06..9ae4048 100644 --- a/tool_cli.py +++ b/tool_cli.py @@ -231,7 +231,12 @@ def main(argv: list[str] | None = None) -> int: "error": {"type": type(exc).__name__, "message": str(exc)}} rendered = json.dumps(result, indent=2, ensure_ascii=False, default=str) + "\n" if args.output: - _write_atomic(args.output, rendered) + try: + _write_atomic(args.output, rendered) + except OSError as exc: + result = {"profile": PROFILE, "status": "failed", + "error": {"type": type(exc).__name__, "message": str(exc)}} + rendered = json.dumps(result, indent=2, ensure_ascii=False) + "\n" sys.stdout.write(rendered) return 1 if result.get("status") == "failed" else 0 From 0ce4e29f54b9a828dc5540bd7a0399d7cf30095e Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:54:41 +0800 Subject: [PATCH 15/23] test(ui): check offline viewer DOM contract and optional JS syntax --- tests/test_tool_ui.py | 59 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/test_tool_ui.py diff --git a/tests/test_tool_ui.py b/tests/test_tool_ui.py new file mode 100644 index 0000000..97669b0 --- /dev/null +++ b/tests/test_tool_ui.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Static offline UI contract checks (not visual/browser accessibility certification).""" +import shutil +import subprocess +import tempfile +import unittest +from html.parser import HTMLParser +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +UI = ROOT / "ui/tri_repo_console.html" + + +class Elements(HTMLParser): + def __init__(self): + super().__init__() + self.ids = [] + self.viewport = False + self.external_scripts = [] + def handle_starttag(self, tag, attrs): + data = dict(attrs) + if "id" in data: + self.ids.append(data["id"]) + if tag == "meta" and data.get("name") == "viewport": + self.viewport = True + if tag == "script" and data.get("src"): + self.external_scripts.append(data["src"]) + + +class TestOfflineUI(unittest.TestCase): + def test_static_document_and_upload_semantics(self): + content = UI.read_text(encoding="utf-8") + parser = Elements() + parser.feed(content) + self.assertTrue(parser.viewport) + self.assertFalse(parser.external_scripts) + self.assertEqual(len(parser.ids), len(set(parser.ids))) + for element in ("drop", "file", "repo", "status", "count", "steps", "raw"): + self.assertIn(element, parser.ids) + self.assertIn("textContent", content) + self.assertNotIn(".innerHTML", content) + self.assertNotIn("fetch(", content) + self.assertIn("focus-visible", content) + + def test_inline_javascript_parses_when_node_available(self): + node = shutil.which("node") + if not node: + self.skipTest("Node.js not installed; UI JS parse check optional") + source = UI.read_text(encoding="utf-8").split("", 1)[0] + with tempfile.TemporaryDirectory() as root: + path = Path(root) / "console.js" + path.write_text(source, encoding="utf-8") + result = subprocess.run([node, "--check", str(path)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + + +if __name__ == "__main__": + unittest.main() From d22102799e09e8da8aa546915401c3450ee716b1 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:55:03 +0800 Subject: [PATCH 16/23] test: assert IO failure returns structured receipt and nonzero exit --- tests/test_tool_cli.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_tool_cli.py b/tests/test_tool_cli.py index 1734503..1ff99a6 100644 --- a/tests/test_tool_cli.py +++ b/tests/test_tool_cli.py @@ -54,6 +54,20 @@ def test_agent_rejects_duplicate_ids_and_unbounded_plans(self): {"id": str(i), "tool": "unknown", "arguments": {}} for i in range(33) ]}) + def test_missing_output_directory_parent_is_structured_failure(self): + with tempfile.TemporaryDirectory() as root: + file_parent = Path(root) / "not-a-directory" + file_parent.write_text("occupied", encoding="utf-8") + result = subprocess.run( + [sys.executable, "tool_cli.py", "list", "--output", + str(file_parent / "receipt.json")], + cwd=ROOT, capture_output=True, text=True, + ) + self.assertNotEqual(result.returncode, 0) + value = json.loads(result.stdout) + self.assertEqual(value["status"], "failed") + self.assertEqual(value["error"]["type"], "FileExistsError") + def test_cli_list_and_atomic_receipt(self): with tempfile.TemporaryDirectory() as root: output = Path(root) / "receipt.json" From 5927a478dde4829a7048d47aebb931826fb382cf Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:55:17 +0800 Subject: [PATCH 17/23] test: reject invalid figure recipe with structured error --- tests/test_tool_cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_tool_cli.py b/tests/test_tool_cli.py index 1ff99a6..364019e 100644 --- a/tests/test_tool_cli.py +++ b/tests/test_tool_cli.py @@ -91,6 +91,14 @@ def test_real_preflight_on_bundled_example(self): self.assertFalse(receipt["output"]["figure_generated"]) self.assertFalse(receipt["output"]["scientific_validity_claim"]) + def test_invalid_recipe_schema_is_structured_failure(self): + with tempfile.TemporaryDirectory() as root: + path = Path(root) / "invalid.yaml" + path.write_text("id: invalid\ntype: line-chart\n", encoding="utf-8") + result = tool_cli.invoke("figure.preflight", {"recipe_path": str(path)}) + self.assertEqual(result["status"], "failed") + self.assertIn(result["error"]["type"], {"ValidationError", "ValueError"}) + def test_profile_path_escape_rejected(self): receipt = tool_cli.invoke("figure.preflight", { "recipe_path": "examples/line_chart.yaml", From 206b958e37c6826b340c0afb53719a6806d5fa21 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:55:35 +0800 Subject: [PATCH 18/23] test(ui): include offline console checks in local make test --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e2617f1..e88dc8c 100644 --- a/Makefile +++ b/Makefile @@ -22,4 +22,4 @@ test-accessibility: .PHONY: test-tool-interface test-tool-interface: - python3 -m unittest tests.test_tool_cli -v + python3 -m unittest tests.test_tool_cli tests.test_tool_ui -v From ea56ba3bc2cd27f5c9d4b97095429e991e8324e2 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:56:33 +0800 Subject: [PATCH 19/23] docs: route new Tool CLI agent and UI sources without changing research history --- docs/03-maintenance-and-audit/DOCUMENT_STATUS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/03-maintenance-and-audit/DOCUMENT_STATUS.md b/docs/03-maintenance-and-audit/DOCUMENT_STATUS.md index 634c061..423a8a7 100644 --- a/docs/03-maintenance-and-audit/DOCUMENT_STATUS.md +++ b/docs/03-maintenance-and-audit/DOCUMENT_STATUS.md @@ -102,6 +102,12 @@ later template revision != earlier stage rewrite Instantiated Stage and Part records remain bounded by their declared evidence window and executed method. Later corrections move interpretation forward without silently rewriting earlier research artifacts. +## Tool / CLI / Agent interface router — 2026-10-19 (additive) + +The new current implementation surface is `tool_cli.py` with its read-only allowlisted core calls, deterministic declared agent-plan runner, and `tests/test_tool_cli.py` as an optional local contract check. The active invocation/boundary document is `docs/02-examples-and-contracts/TOOL_CLI_AGENT_CONTRACT.md`, and `examples/tool_plan.json` is a synthetic usage fixture. The additional current presentation surfaces are `ui/tri_repo_console.html`, `docs/02-examples-and-contracts/TOOL_UI_STYLE.md` and `tests/test_tool_ui.py`; they consume local receipts without using the experimental observer dashboard. + +This additive router does not advance the recorded Stage/calendar baseline, rewrite historical snapshots, certify MCP compliance, imply LLM agent execution, or assert a full GitHub CI/independent reproducibility result. Real behavior remains subordinate to the current code and machine contract; a Draft PR is not merged authority. + ## Historical and dated evidence Dated maintenance records and materials under `history/` remain point-in-time evidence. The August stage, frontier material, FOUR/FIVE/SIX_DAY consolidations, correction records, demonstrations, and later dated reconciliations remain recoverable at their original time boundary. From fd25562fc3a7d9b67f742ebb4db032db6dbd5a88 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:57:26 +0800 Subject: [PATCH 20/23] docs(examples): connect baseline recipe, native tool request, agent plan and offline console --- examples/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/README.md b/examples/README.md index 1af3145..bbd886c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,6 +5,19 @@ They are not GitHub workflow instructions and do not imply scientific validation See [../DOCUMENT_STATUS.md](../DOCUMENT_STATUS.md) for current-versus-historical document authority +## Native Tool / CLI / Agent and offline console + +The missing baseline fixture `examples/line_chart.yaml` is now supplied for the documented render command. The recipe uses **synthetic illustrative data**, not measured science or a publication-ready claim. + +```bash +python3 tool_cli.py list +python3 tool_cli.py call figure.preflight --input examples/tool_request.json +python3 tool_cli.py agent --input examples/tool_plan.json --output output/agent-plan.json +python3 sci_render.py examples/line_chart.yaml --profile nature --backend matplotlib +``` + +Open `ui/tri_repo_console.html` in a local browser and select an exported Tool/Agent JSON receipt. It is a **read-only offline review UI**, not a real-time renderer or MCP server. Read `docs/02-examples-and-contracts/TOOL_UI_STYLE.md` for design/interaction constraints. + ## Baseline render ```bash From 1072be969ec8b93284a737c8f09a94b5c0dd9e13 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:57:37 +0800 Subject: [PATCH 21/23] examples: add native preflight CLI request fixture --- examples/tool_request.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 examples/tool_request.json diff --git a/examples/tool_request.json b/examples/tool_request.json new file mode 100644 index 0000000..3dd5515 --- /dev/null +++ b/examples/tool_request.json @@ -0,0 +1,4 @@ +{ + "recipe_path": "examples/line_chart.yaml", + "profile_name": "nature" +} From eb55fd60ed61794fe1bad95271872ece985ea8b5 Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:59:36 +0800 Subject: [PATCH 22/23] fix(examples): provide HTML-compatible Observable recipe fixture --- examples/line_chart_observable.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 examples/line_chart_observable.yaml diff --git a/examples/line_chart_observable.yaml b/examples/line_chart_observable.yaml new file mode 100644 index 0000000..aafeb2b --- /dev/null +++ b/examples/line_chart_observable.yaml @@ -0,0 +1,21 @@ +# Synthetic data: optional Observable HTML fixture, not offline/render verification +id: line-chart-observable-example +name: Synthetic two-series HTML output +type: line-chart +data: + control: [1, 2, 3, 4] + treatment: [2, 3, 4, 5] +aesthetics: + title: Synthetic observations (fixture) + x_label: Sample + y_label: Value (arbitrary units) + palette: ["#0072B2", "#D55E00"] +accessibility: + require_alt_text: true + alt_text: "Synthetic example, two increasing series across four samples." + redundant_encoding: off +output: + dir: output + filename: line_chart_observable.html + format: html +notes: "HTML output requires optional Node/browser/CDN runtime; not an offline or publisher validity claim." From 3d87ccb73679a347de07728082f6b5ee2b0342aa Mon Sep 17 00:00:00 2001 From: lostlight530 <13957086206@163.com> Date: Mon, 19 Oct 2026 22:59:51 +0800 Subject: [PATCH 23/23] docs(examples): correct Observable HTML backend recipe mismatch --- examples/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index bbd886c..c40e0d3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -172,7 +172,7 @@ The renderer preserves this declaration but does not validate the upstream stati ```bash python3 sci_render.py examples/line_chart.yaml --profile presentation --backend ggplot2 -python3 sci_render.py examples/line_chart.yaml --profile presentation --backend observable +python3 sci_render.py examples/line_chart_observable.yaml --profile presentation --backend observable ``` Adapter source presence does not prove optional R/Node runtime availability @@ -228,3 +228,7 @@ The dated first complete three-cadence demonstration is recorded under `maintena ## Local maintenance boundary Local checks are optional maintenance aids, not GitHub merge gates, scientific validation, WCAG certification or publisher acceptance + +### Backend-specific fixture boundary (2026-10-19) + +The default `examples/line_chart.yaml` requests PNG. Observable supports only HTML in the current backend matrix; its example uses a separate `line_chart_observable.yaml` with `output.format: html`. This is a synthetic fixture and requires an available Node runtime plus browser-side CDN dependency; no offline browser execution or feature parity with Matplotlib is implied.