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
+
+
+
+
+
+
+
+
+
+仓库未加载
+执行状态—
+已执行步骤—
+证据来源本地文件
+
+
+原始证据
只读 · 不修改展开 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.