From 7a1decf0145b3eb503466718571d3e2955bb40ab Mon Sep 17 00:00:00 2001 From: zhanghan <2774877132@qq.com> Date: Thu, 13 Aug 2026 00:12:07 +0800 Subject: [PATCH 1/5] ci: add repository verification workflow --- .github/workflows/ci.yml | 72 +++++++++ LICENSE | 211 +++++++++++++++++++++++++-- README.md | 2 + package-lock.json | 19 ++- package.json | 3 +- python/standalone_reproduce.py | 4 +- tests/orchestrator.test.ts | 14 +- tests/repository-engineering.test.ts | 122 ++++++++++++++++ 8 files changed, 426 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/repository-engineering.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..517bb39 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Node.js dependencies + run: npm ci + + - name: Install Python dependencies + run: python3 -m pip install -r python/requirements.lock + + - name: Check + run: npm run check + + - name: Build + run: npm run build + + - name: Smoke + run: | + set -euo pipefail + RUNS_ROOT="$(mktemp -d)" + export MODELING_AGENT_RUNS_ROOT="$RUNS_ROOT" + npm run smoke + + archive_count="$(find "$RUNS_ROOT" -type f -name 'project.zip' -print | wc -l)" + test "$archive_count" -eq 1 + archive="$(find "$RUNS_ROOT" -type f -name 'project.zip' -print -quit)" + + isolated_root="$(mktemp -d)" + cp "$archive" "$isolated_root/project.zip" + unzip -q "$isolated_root/project.zip" -d "$isolated_root/project" + rm -rf "$RUNS_ROOT" + unset MODELING_AGENT_RUNS_ROOT + + cd "$isolated_root/project" + python3 reproduce.py + + python3 - <<'PY' + import json + from pathlib import Path + + result = json.loads(Path("reproduced/reproduction-result.json").read_text(encoding="utf-8")) + artifact_count = sum(task.get("verified_artifact_count", 0) for task in result.get("tasks", [])) + assert result.get("status") == "success", result + assert result.get("verified_task_count") == 9, result + assert artifact_count == 40, {"artifact_count": artifact_count, "result": result} + assert result.get("warning") is None, result + pdf = Path("reproduced") / result["report_pdf"] + assert pdf.read_bytes().startswith(b"%PDF-"), pdf + PY diff --git a/LICENSE b/LICENSE index b653997..8137309 100644 --- a/LICENSE +++ b/LICENSE @@ -1,17 +1,202 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ -Copyright 2026 modeling-agent contributors + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - http://www.apache.org/licenses/LICENSE-2.0 + 1. Definitions. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 modeling-agent contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index a746ddf..c32d791 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Modeling Agent +[![CI](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml) + `modeling-agent` is a local mathematical-modeling competition agent. It turns a problem package into a typed task graph, executes real Python experiments, records an evidence trail, and exports a reproducible project package. The current target is `v0.1-alpha`: broad support for common task families with explicit evidence levels. It is not a claim that every modeling problem can be solved reliably or at award-winning quality. diff --git a/package-lock.json b/package-lock.json index f39e3a0..72a350e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "@types/node": "24.10.1", "tsx": "4.23.12", "typescript": "5.9.3", - "vitest": "4.1.10" + "vitest": "4.1.10", + "yaml": "2.9.0" }, "engines": { "node": ">=24 <25" @@ -4265,6 +4266,22 @@ "engines": { "node": ">=8" } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/package.json b/package.json index 6aff8a0..299fa93 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/node": "24.10.1", "tsx": "4.23.12", "typescript": "5.9.3", - "vitest": "4.1.10" + "vitest": "4.1.10", + "yaml": "2.9.0" } } diff --git a/python/standalone_reproduce.py b/python/standalone_reproduce.py index 87149f3..55383c6 100644 --- a/python/standalone_reproduce.py +++ b/python/standalone_reproduce.py @@ -199,9 +199,7 @@ def _build_report() -> tuple[str, str | None]: if completed.returncode == 0 and (deliverables / "report.pdf").is_file(): renderer = "xelatex" else: - warning = f"xelatex exited with {completed.returncode}" - else: - warning = "xelatex is unavailable" + warning = f"xelatex exited with {completed.returncode}; used the bundled PDF renderer" if renderer == "builtin": title = _read_json(REPRODUCIBILITY / "problem-spec.json").get("title", "Reproduced report") (deliverables / "report.pdf").write_bytes(_fallback_pdf(str(title), markdown.read_text(encoding="utf-8"))) diff --git a/tests/orchestrator.test.ts b/tests/orchestrator.test.ts index c027a9a..f27150d 100644 --- a/tests/orchestrator.test.ts +++ b/tests/orchestrator.test.ts @@ -116,7 +116,7 @@ describe("fake local vertical slice", () => { "reproducibility/python/modeling_agent/runner.py" ])); const projectReadme = await readFile(resolve(projectRoot, "README.md"), "utf8"); - expect(projectReadme).toContain("The frozen experiment records were originally produced and verified with Python 3.10.12."); + expect(projectReadme).toMatch(/The frozen experiment records were originally produced and verified with Python 3\.\d+\.\d+\./); expect(projectReadme).toContain("a Python environment compatible with the packaged `requirements.lock`"); expect(projectReadme).toContain("the packaged Dockerfile's fixed environment"); expect(projectReadme).not.toContain("Python 3.10.12 is required"); @@ -217,8 +217,16 @@ describe("fake local vertical slice", () => { const reproduced = await runCommand("python3", ["reproduce.py"], unpacked, { ...process.env, PYTHONPATH: "" }); expect(reproduced.code, `${reproduced.stdout}\n${reproduced.stderr}`).toBe(0); - const manifest = JSON.parse(await readFile(resolve(unpacked, "reproduced", "reproduction-result.json"), "utf8")) as { task_count: number; verified_task_count: number; report_pdf: string }; - expect(manifest).toMatchObject({ task_count: 9, verified_task_count: 9, report_pdf: "deliverables/report.pdf" }); + const manifest = JSON.parse(await readFile(resolve(unpacked, "reproduced", "reproduction-result.json"), "utf8")) as { + status: string; + task_count: number; + verified_task_count: number; + tasks: Array<{ verified_artifact_count: number }>; + report_pdf: string; + warning: string | null; + }; + expect(manifest).toMatchObject({ status: "success", task_count: 9, verified_task_count: 9, report_pdf: "deliverables/report.pdf", warning: null }); + expect(manifest.tasks.reduce((total, task) => total + task.verified_artifact_count, 0)).toBe(40); expect((await readFile(resolve(unpacked, "reproduced", "deliverables", "report.pdf"))).subarray(0, 5).toString()).toBe("%PDF-"); expect((await readdir(resolve(unpacked, "reproduced", "experiments"))).sort()).toHaveLength(9); }, 180_000); diff --git a/tests/repository-engineering.test.ts b/tests/repository-engineering.test.ts new file mode 100644 index 0000000..ce106af --- /dev/null +++ b/tests/repository-engineering.test.ts @@ -0,0 +1,122 @@ +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; + +const workflowPath = ".github/workflows/ci.yml"; + +interface WorkflowStep { + name?: string; + uses?: string; + with?: Record; + run?: string; +} + +interface WorkflowJob { + "runs-on"?: string; + permissions?: Record; + steps?: WorkflowStep[]; +} + +interface Workflow { + name?: string; + on?: unknown; + permissions?: Record; + jobs?: Record; +} + +function workflowCommands(steps: WorkflowStep[]): string[] { + return steps.flatMap((step) => typeof step.run === "string" ? [step.run] : []); +} + +function stepUsing(steps: WorkflowStep[], action: string): WorkflowStep { + const step = steps.find((candidate) => candidate.uses?.startsWith(`${action}@`)); + expect(step, `missing ${action} step`).toBeDefined(); + return step!; +} + +function expectCommand(commands: string[], command: string): void { + expect(commands.some((script) => script.split("\n").some((line) => line.trim() === command)), `missing exact command: ${command}`).toBe(true); +} + +describe("repository engineering contract", () => { + it("defines a structurally valid least-privilege CI job with pinned runtimes and npm lifecycle commands", async () => { + const source = await readFile(workflowPath, "utf8"); + const workflow = parse(source) as Workflow; + const jobs = Object.values(workflow.jobs ?? {}); + expect(jobs).toHaveLength(1); + + const job = jobs[0]!; + expect(job["runs-on"]).toBe("ubuntu-latest"); + expect(workflow.permissions).toEqual({ contents: "read" }); + expect(job.permissions).toBeUndefined(); + + const steps = job.steps ?? []; + expect(stepUsing(steps, "actions/checkout").uses).toBe("actions/checkout@v4"); + expect(stepUsing(steps, "actions/setup-node")).toMatchObject({ + uses: "actions/setup-node@v4", + with: { "node-version": 24, cache: "npm" } + }); + expect(stepUsing(steps, "actions/setup-python")).toMatchObject({ + uses: "actions/setup-python@v5", + with: { "python-version": "3.11" } + }); + + const commands = workflowCommands(steps); + for (const command of ["npm ci", "npm run check", "npm run build", "npm run smoke"]) { + expectCommand(commands, command); + } + }); + + it("reproduces the fresh smoke archive independently and machine-checks the complete result", async () => { + const workflow = parse(await readFile(workflowPath, "utf8")) as Workflow; + const steps = Object.values(workflow.jobs ?? {})[0]?.steps ?? []; + const scripts = workflowCommands(steps).join("\n"); + + expect(scripts).toContain("mktemp -d"); + expect(scripts).toContain("MODELING_AGENT_RUNS_ROOT"); + expect(scripts).toMatch(/find\s+"?\$\{?RUNS_ROOT\}?"?\s+-type f -name ['\"]project\\?\.zip['\"]/); + expect(scripts).toMatch(/cp\s+.*project.*\.zip/i); + expect(scripts).toMatch(/unzip\s+.*-d/); + expect(scripts).toMatch(/rm\s+-rf\s+"?\$\{?RUNS_ROOT\}?"?/); + expect(scripts).toContain("python3 reproduce.py"); + expect(scripts).toContain("reproduced/reproduction-result.json"); + expect(scripts).toContain("verified_artifact_count"); + expect(scripts).toContain("verified_task_count"); + expect(scripts).toContain("%PDF-"); + expect(scripts).toMatch(/status[^\n]*success/); + expect(scripts).toMatch(/verified_task_count[^\n]*9/); + expect(scripts).toMatch(/artifact[^\n]*40/i); + expect(scripts).toMatch(/warning[^\n]*(?:None|null)/); + expect(scripts).toMatch(/reproduced[^\n]*report_pdf/); + expect(scripts).not.toMatch(/run-[0-9]{8,}/); + expect(scripts).not.toMatch(/docker\s+(?:build|push)|--runtime\s+pi|runs\.sqlite|secrets\./i); + }); + + it("shows the CI badge and carries the complete standard Apache License 2.0 text", async () => { + const [readme, license] = await Promise.all([ + readFile("README.md", "utf8"), + readFile("LICENSE", "utf8") + ]); + const firstSection = readme.split("\n\n", 2).join("\n\n"); + expect(firstSection).toContain("[![CI](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml)"); + + for (const passage of [ + "Apache License\n Version 2.0, January 2004", + "1. Definitions.", + "2. Grant of Copyright License.", + "3. Grant of Patent License.", + "4. Redistribution.", + "5. Submission of Contributions.", + "6. Trademarks.", + "7. Disclaimer of Warranty.", + "8. Limitation of Liability.", + "9. Accepting Warranty or Additional Liability.", + "END OF TERMS AND CONDITIONS", + "APPENDIX: How to apply the Apache License to your work.", + "Copyright 2026 modeling-agent contributors" + ]) { + expect(license).toContain(passage); + } + expect(license.length).toBeGreaterThan(10_000); + }); +}); From e44fca4bb590b75ac141d258befb1c91cdbdc834 Mon Sep 17 00:00:00 2001 From: zhanghan <2774877132@qq.com> Date: Thu, 13 Aug 2026 00:32:03 +0800 Subject: [PATCH 2/5] fix: preserve standalone fallback warnings --- .github/workflows/ci.yml | 14 ++++- python/standalone_reproduce.py | 4 +- python/tests/test_standalone_reproduce.py | 70 +++++++++++++++++++++++ tests/orchestrator.test.ts | 11 +++- tests/repository-engineering.test.ts | 17 +++++- 5 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 python/tests/test_standalone_reproduce.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 517bb39..66898e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [main] pull_request: permissions: @@ -66,7 +67,18 @@ jobs: assert result.get("status") == "success", result assert result.get("verified_task_count") == 9, result assert artifact_count == 40, {"artifact_count": artifact_count, "result": result} - assert result.get("warning") is None, result + renderer = result.get("report_renderer") + warning = result.get("warning") + if renderer == "xelatex": + assert warning is None, result + elif renderer == "builtin": + assert isinstance(warning, str) and warning, result + normalized_warning = warning.lower() + assert "xelatex" in normalized_warning, result + assert "unavailable" in normalized_warning or "fail" in normalized_warning, result + assert "bundled" in normalized_warning and "fallback" in normalized_warning, result + else: + raise AssertionError(result) pdf = Path("reproduced") / result["report_pdf"] assert pdf.read_bytes().startswith(b"%PDF-"), pdf PY diff --git a/python/standalone_reproduce.py b/python/standalone_reproduce.py index 55383c6..72a2ee8 100644 --- a/python/standalone_reproduce.py +++ b/python/standalone_reproduce.py @@ -199,7 +199,9 @@ def _build_report() -> tuple[str, str | None]: if completed.returncode == 0 and (deliverables / "report.pdf").is_file(): renderer = "xelatex" else: - warning = f"xelatex exited with {completed.returncode}; used the bundled PDF renderer" + warning = f"XeLaTeX failed with exit code {completed.returncode}; used the bundled PDF fallback renderer" + else: + warning = "XeLaTeX is unavailable; used the bundled PDF fallback renderer" if renderer == "builtin": title = _read_json(REPRODUCIBILITY / "problem-spec.json").get("title", "Reproduced report") (deliverables / "report.pdf").write_bytes(_fallback_pdf(str(title), markdown.read_text(encoding="utf-8"))) diff --git a/python/tests/test_standalone_reproduce.py b/python/tests/test_standalone_reproduce.py new file mode 100644 index 0000000..c5c8f5b --- /dev/null +++ b/python/tests/test_standalone_reproduce.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import standalone_reproduce + + +class StandaloneReportTests(unittest.TestCase): + def setUp(self) -> None: + temporary_directory = tempfile.TemporaryDirectory(prefix="standalone-report-") + self.addCleanup(temporary_directory.cleanup) + self.root = Path(temporary_directory.name) + deliverables = self.root / "deliverables" + deliverables.mkdir() + (deliverables / "report.md").write_text("# Deterministic report\n", encoding="utf-8") + (deliverables / "report.tex").write_text("\\documentclass{article}\n", encoding="utf-8") + self.reproducibility = self.root / "reproducibility" + self.reproducibility.mkdir() + (self.reproducibility / "problem-spec.json").write_text( + json.dumps({"title": "Deterministic report"}), + encoding="utf-8", + ) + self.output = self.root / "reproduced" + + def build_report(self) -> tuple[str, str | None]: + with ( + patch.object(standalone_reproduce, "ROOT", self.root), + patch.object(standalone_reproduce, "REPRODUCIBILITY", self.reproducibility), + patch.object(standalone_reproduce, "OUTPUT", self.output), + ): + return standalone_reproduce._build_report() + + def assert_builtin_fallback_warning(self, warning: str | None, reason: str) -> None: + self.assertIsInstance(warning, str) + normalized = warning.lower() + for phrase in ("xelatex", reason, "bundled", "fallback"): + self.assertIn(phrase, normalized) + self.assertTrue((self.output / "deliverables" / "report.pdf").read_bytes().startswith(b"%PDF-")) + + def test_missing_xelatex_uses_builtin_renderer_with_honest_warning(self) -> None: + with patch.object(standalone_reproduce.shutil, "which", return_value=None) as which: + renderer, warning = self.build_report() + + which.assert_called_once_with("xelatex") + self.assertEqual(renderer, "builtin") + self.assert_builtin_fallback_warning(warning, "unavailable") + + def test_failed_xelatex_uses_builtin_renderer_and_preserves_warning(self) -> None: + failure = subprocess.CompletedProcess(args=["xelatex"], returncode=9, stdout="", stderr="compile failed") + with ( + patch.object(standalone_reproduce.shutil, "which", return_value="/mock/xelatex"), + patch.object(standalone_reproduce.subprocess, "run", return_value=failure) as run, + ): + renderer, warning = self.build_report() + + run.assert_called_once() + self.assertEqual(renderer, "builtin") + self.assert_builtin_fallback_warning(warning, "failed") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/orchestrator.test.ts b/tests/orchestrator.test.ts index f27150d..5fcb33c 100644 --- a/tests/orchestrator.test.ts +++ b/tests/orchestrator.test.ts @@ -223,9 +223,18 @@ describe("fake local vertical slice", () => { verified_task_count: number; tasks: Array<{ verified_artifact_count: number }>; report_pdf: string; + report_renderer: "xelatex" | "builtin"; warning: string | null; }; - expect(manifest).toMatchObject({ status: "success", task_count: 9, verified_task_count: 9, report_pdf: "deliverables/report.pdf", warning: null }); + expect(manifest).toMatchObject({ status: "success", task_count: 9, verified_task_count: 9, report_pdf: "deliverables/report.pdf" }); + if (manifest.report_renderer === "xelatex") { + expect(manifest.warning).toBeNull(); + } else { + expect(manifest.report_renderer).toBe("builtin"); + expect(manifest.warning).toMatch(/xelatex/i); + expect(manifest.warning).toMatch(/(?:unavailable|fail)/i); + expect(manifest.warning).toMatch(/bundled.*fallback/i); + } expect(manifest.tasks.reduce((total, task) => total + task.verified_artifact_count, 0)).toBe(40); expect((await readFile(resolve(unpacked, "reproduced", "deliverables", "report.pdf"))).subarray(0, 5).toString()).toBe("%PDF-"); expect((await readdir(resolve(unpacked, "reproduced", "experiments"))).sort()).toHaveLength(9); diff --git a/tests/repository-engineering.test.ts b/tests/repository-engineering.test.ts index ce106af..7b885aa 100644 --- a/tests/repository-engineering.test.ts +++ b/tests/repository-engineering.test.ts @@ -17,9 +17,14 @@ interface WorkflowJob { steps?: WorkflowStep[]; } +interface WorkflowTriggers { + push?: { branches?: string[] }; + pull_request?: unknown; +} + interface Workflow { name?: string; - on?: unknown; + on?: WorkflowTriggers; permissions?: Record; jobs?: Record; } @@ -44,6 +49,10 @@ describe("repository engineering contract", () => { const workflow = parse(source) as Workflow; const jobs = Object.values(workflow.jobs ?? {}); expect(jobs).toHaveLength(1); + expect(workflow.on).toEqual({ + push: { branches: ["main"] }, + pull_request: null + }); const job = jobs[0]!; expect(job["runs-on"]).toBe("ubuntu-latest"); @@ -86,7 +95,13 @@ describe("repository engineering contract", () => { expect(scripts).toMatch(/status[^\n]*success/); expect(scripts).toMatch(/verified_task_count[^\n]*9/); expect(scripts).toMatch(/artifact[^\n]*40/i); + expect(scripts).toContain('renderer = result.get("report_renderer")'); + expect(scripts).toContain('if renderer == "xelatex":'); + expect(scripts).toContain('elif renderer == "builtin":'); expect(scripts).toMatch(/warning[^\n]*(?:None|null)/); + expect(scripts).toContain('assert "xelatex" in normalized_warning, result'); + expect(scripts).toContain('assert "unavailable" in normalized_warning or "fail" in normalized_warning, result'); + expect(scripts).toContain('assert "bundled" in normalized_warning and "fallback" in normalized_warning, result'); expect(scripts).toMatch(/reproduced[^\n]*report_pdf/); expect(scripts).not.toMatch(/run-[0-9]{8,}/); expect(scripts).not.toMatch(/docker\s+(?:build|push)|--runtime\s+pi|runs\.sqlite|secrets\./i); From 2ad5794d3570b2a3749a15524ad00699f4a9f2da Mon Sep 17 00:00:00 2001 From: zhanghan <2774877132@qq.com> Date: Thu, 13 Aug 2026 00:51:37 +0800 Subject: [PATCH 3/5] ci: use Node 24 action runtimes --- .github/workflows/ci.yml | 6 +++--- tests/repository-engineering.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66898e4..6c824a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,16 +13,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: 24 cache: npm - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.11" diff --git a/tests/repository-engineering.test.ts b/tests/repository-engineering.test.ts index 7b885aa..9d033de 100644 --- a/tests/repository-engineering.test.ts +++ b/tests/repository-engineering.test.ts @@ -60,13 +60,13 @@ describe("repository engineering contract", () => { expect(job.permissions).toBeUndefined(); const steps = job.steps ?? []; - expect(stepUsing(steps, "actions/checkout").uses).toBe("actions/checkout@v4"); + expect(stepUsing(steps, "actions/checkout").uses).toBe("actions/checkout@v7"); expect(stepUsing(steps, "actions/setup-node")).toMatchObject({ - uses: "actions/setup-node@v4", + uses: "actions/setup-node@v7", with: { "node-version": 24, cache: "npm" } }); expect(stepUsing(steps, "actions/setup-python")).toMatchObject({ - uses: "actions/setup-python@v5", + uses: "actions/setup-python@v7", with: { "python-version": "3.11" } }); From e8de283bb982ec99b57b040b86048600e91398d5 Mon Sep 17 00:00:00 2001 From: zhanghan <2774877132@qq.com> Date: Thu, 13 Aug 2026 01:27:37 +0800 Subject: [PATCH 4/5] docs: translate README to Chinese --- README.md | 62 ++++++++++++++----------- tests/repository-engineering.test.ts | 68 +++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index c32d791..473bdee 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ [![CI](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml) -`modeling-agent` is a local mathematical-modeling competition agent. It turns a problem package into a typed task graph, executes real Python experiments, records an evidence trail, and exports a reproducible project package. +> **徽章说明:**绿色 / `passing` 表示自动测试与构建通过,红色 / `failing` 表示失败;点击徽章可查看 GitHub Actions 日志。徽章展示的是对应工作流最近一次运行的状态,并不保证它始终为绿色。 -The current target is `v0.1-alpha`: broad support for common task families with explicit evidence levels. It is not a claim that every modeling problem can be solved reliably or at award-winning quality. +`modeling-agent` 是一个在本地运行、面向数学建模竞赛的单用户智能体。它会读取题目材料包,把全部要求分解为带类型的任务图,运行真实且可复现的 Python 实验,记录从结论到证据的追踪关系,并导出一个完整、可复现的项目包。 -## Architecture +当前目标版本是 `v0.1-alpha`:以明确的证据等级,尽力覆盖常见任务族。当前版本并不成熟;这既不意味着它能可靠解决所有建模问题,也不代表具备获奖级表现。在缺少基准测试证据时,不应作出成熟或获奖级能力声明。 + +## 架构 ```text CLI / local Web UI @@ -23,31 +25,31 @@ Docker Python experiment workers Evidence Graph -> report -> project package ``` -The outer lifecycle is fixed. A planner creates a Schema-constrained inner task graph using these task families: +外层生命周期固定不变。规划器通过 Schema 约束生成内部任务图;任务族共 9 类: -- statistical analysis -- regression and prediction -- time-series forecasting -- classification -- clustering -- evaluation and ranking -- optimization -- simulation -- experimental fallback for long-tail methods +- 统计分析 +- 回归与预测 +- 时间序列预测 +- 分类 +- 聚类 +- 评价与排序 +- 优化 +- 仿真 +- 面向长尾方法的实验性兜底任务 -## Development Status +## 开发状态 -The repository is being built as a vertical slice. See [the alpha specification](docs/spec/v0.1-alpha.md) and [implementation plan](docs/spec/two-week-plan.md) for the frozen boundary and milestones. +本仓库目前按纵向切片方式开发。[alpha 规格](docs/spec/v0.1-alpha.md)说明已冻结的功能边界,[两周实施计划](docs/spec/two-week-plan.md)列出对应里程碑。 -## Prerequisites +## 前置条件 - Node.js 24 LTS - Python 3.11 - Docker -- XeLaTeX with CTeX for Chinese reports -- OpenCLI for audited web research +- 用于生成中文报告、带 CTeX 的 XeLaTeX +- 用于受审计网络研究的 OpenCLI -## Commands +## 完整命令 ```bash npm ci @@ -59,16 +61,24 @@ npm run cli -- export ./project.zip npm run cli -- reproduce ``` -Use `--runs-root ` before the subcommand to select a separate run database/workspace, and add `--json` to a subcommand for machine-readable output. The fake runtime is deterministic and intended for tests; the pi SDK runtime remains optional and isolated behind `AgentRuntime`. +如需选择独立的运行数据库与工作区,请在子命令前传入 `--runs-root `;如需机器可读输出,请给相应子命令添加 `--json`。fake runtime 是确定性的测试实现;pi SDK runtime 仍为可选能力,并通过 `AgentRuntime` 接口与其余系统隔离。 + +## 执行与报告契约 + +本地执行路径是当前 alpha 阶段支持的基线。若要使用 Docker,请先运行 `docker build -t modeling-agent-python:0.1-alpha python/` 构建固定版本的 worker 镜像,再设置 `MODELING_AGENT_PYTHON_IMAGE=modeling-agent-python:0.1-alpha`,或传入等效的 worker 选项。 + +Docker 执行采用 Python 模块调用,并遵守以下安全契约:容器不能访问网络;根文件系统只读;CPU、内存等资源受限;每个输入以只读方式挂载;只有输出挂载点可写。报告生成始终产出 `report.md`、`report.tex` 和真实的 `report.pdf`。如果 XeLaTeX 不可用或 TeX 源文件编译失败,系统会改用内置 PDF 渲染器,并在 `report-status.json` 中记录明确的兜底告警与限制,而不会静默掩盖降级。 + +## 独立复现包 -The local execution path is the supported alpha baseline. Build the pinned Docker worker image with `docker build -t modeling-agent-python:0.1-alpha python/`, then set `MODELING_AGENT_PYTHON_IMAGE=modeling-agent-python:0.1-alpha` (or pass the equivalent worker option); Docker execution uses module invocation, no network, a read-only root, resource limits, read-only per-input mounts, and a writable output mount. Report generation always emits `report.md`, `report.tex`, and a real `report.pdf`; when XeLaTeX is unavailable or the TeX source cannot compile, a built-in PDF renderer is used and `report-status.json` records the limitation. +每个导出的 `project.zip` 都是独立可复现的软件包:可以在任意目录解压,然后运行 `python3 reproduce.py`。项目包包含冻结的请求与结果、已提交的图表、Python 执行源码、固定依赖清单与 Dockerfile,以及包清单。 -Every exported `project.zip` is standalone: unpack it anywhere and run `python3 reproduce.py`. The package contains frozen requests/results, committed figures and tables, the Python execution source, a pinned requirements manifest and Dockerfile, and a package manifest; reproduction re-executes every frozen request, verifies semantic results plus artifact hashes, and rebuilds `reproduced/deliverables/report.pdf` without the original repository, application, SQLite database, or run ID. +复现流程会重新执行每个冻结请求,校验语义结果和产物哈希,并在 `reproduced/deliverables/report.pdf` 重建报告;整个过程不依赖原始仓库、应用程序、SQLite 数据库或 run ID。 -## Security +## 安全边界 -Generated code is untrusted. It is policy-checked and executed in a constrained container. Network research is mediated by `ResearchGateway`; browser and model credentials are never written to run state or exported projects. +生成的代码一律视为不可信内容:只有通过策略校验后,才能在受资源限制的容器中执行。网络研究统一由 `ResearchGateway` 代理;浏览器凭据和模型凭据绝不会写入运行状态,也不会进入导出的项目包。LLM 输出本身不能充当证据,数值结论必须能通过 Evidence Graph 解析到实际证据。 -## License +## 许可证与材料版权 -Apache-2.0. Competition data, papers, and user-provided materials retain their original rights and are not part of this repository. +本仓库代码采用 Apache-2.0 许可证。竞赛数据、论文及用户提供的其他材料继续归各自权利人所有,不属于本仓库的授权内容。 diff --git a/tests/repository-engineering.test.ts b/tests/repository-engineering.test.ts index 9d033de..3238ceb 100644 --- a/tests/repository-engineering.test.ts +++ b/tests/repository-engineering.test.ts @@ -3,6 +3,25 @@ import { describe, expect, it } from "vitest"; import { parse } from "yaml"; const workflowPath = ".github/workflows/ci.yml"; +const ciBadgeMarkdown = "[![CI](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml)"; + +function readmeExplanatoryText(source: string): string { + let insideFence = false; + return source + .split("\n") + .filter((line) => { + if (line.trim().startsWith("```")) { + insideFence = !insideFence; + return false; + } + return !insideFence; + }) + .join("\n") + .replace(/`[^`\n]+`/g, "") + .replace(/!\[[^\]]*\]\([^)]*\)/g, "") + .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") + .replace(/https?:\/\/\S+/g, ""); +} interface WorkflowStep { name?: string; @@ -107,14 +126,49 @@ describe("repository engineering contract", () => { expect(scripts).not.toMatch(/docker\s+(?:build|push)|--runtime\s+pi|runs\.sqlite|secrets\./i); }); - it("shows the CI badge and carries the complete standard Apache License 2.0 text", async () => { - const [readme, license] = await Promise.all([ - readFile("README.md", "utf8"), - readFile("LICENSE", "utf8") - ]); - const firstSection = readme.split("\n\n", 2).join("\n\n"); - expect(firstSection).toContain("[![CI](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml)"); + it("documents the project primarily in Chinese and explains the exact CI badge", async () => { + const readme = await readFile("README.md", "utf8"); + const badgeOffset = readme.indexOf(ciBadgeMarkdown); + expect(badgeOffset).toBeGreaterThanOrEqual(0); + + const textAfterBadge = readme.slice(badgeOffset + ciBadgeMarkdown.length); + expect(textAfterBadge).toMatch(/^\n\n> \*\*徽章说明:\*\*/); + for (const explanation of [ + "绿色 / `passing` 表示自动测试与构建通过", + "红色 / `failing` 表示失败", + "点击徽章可查看 GitHub Actions 日志" + ]) { + expect(textAfterBadge.slice(0, 180)).toContain(explanation); + } + + for (const passage of [ + "## 架构", + "## 开发状态", + "## 前置条件", + "## 完整命令", + "## 安全边界", + "## 许可证与材料版权", + "当前版本并不成熟", + "不代表具备获奖级表现", + "任务族共 9 类", + "本地执行路径是当前 alpha 阶段支持的基线", + "每个导出的 `project.zip` 都是独立可复现的软件包" + ]) { + expect(readme).toContain(passage); + } + + const explanatoryText = readmeExplanatoryText(readme); + const chineseCharacterCount = explanatoryText.match(/\p{Script=Han}/gu)?.length ?? 0; + const latinCharacterCount = explanatoryText.match(/[A-Za-z]/g)?.length ?? 0; + expect(chineseCharacterCount, "README should contain substantial Chinese explanatory prose").toBeGreaterThan(500); + expect( + chineseCharacterCount / (chineseCharacterCount + latinCharacterCount), + "Chinese characters should be the clear majority of non-code explanatory language characters" + ).toBeGreaterThan(0.6); + }); + it("carries the complete standard Apache License 2.0 text", async () => { + const license = await readFile("LICENSE", "utf8"); for (const passage of [ "Apache License\n Version 2.0, January 2004", "1. Definitions.", From e4e25f905d69184d972bce3a536b6fa984efbb3e Mon Sep 17 00:00:00 2001 From: zhanghan <2774877132@qq.com> Date: Thu, 13 Aug 2026 01:49:50 +0800 Subject: [PATCH 5/5] docs: remove CI badge status legend --- README.md | 2 -- tests/repository-engineering.test.ts | 18 +++++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 473bdee..c59e4c9 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ [![CI](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/ZZDR1023/modeling-agent/actions/workflows/ci.yml) -> **徽章说明:**绿色 / `passing` 表示自动测试与构建通过,红色 / `failing` 表示失败;点击徽章可查看 GitHub Actions 日志。徽章展示的是对应工作流最近一次运行的状态,并不保证它始终为绿色。 - `modeling-agent` 是一个在本地运行、面向数学建模竞赛的单用户智能体。它会读取题目材料包,把全部要求分解为带类型的任务图,运行真实且可复现的 Python 实验,记录从结论到证据的追踪关系,并导出一个完整、可复现的项目包。 当前目标版本是 `v0.1-alpha`:以明确的证据等级,尽力覆盖常见任务族。当前版本并不成熟;这既不意味着它能可靠解决所有建模问题,也不代表具备获奖级表现。在缺少基准测试证据时,不应作出成熟或获奖级能力声明。 diff --git a/tests/repository-engineering.test.ts b/tests/repository-engineering.test.ts index 3238ceb..a27a7cb 100644 --- a/tests/repository-engineering.test.ts +++ b/tests/repository-engineering.test.ts @@ -126,19 +126,15 @@ describe("repository engineering contract", () => { expect(scripts).not.toMatch(/docker\s+(?:build|push)|--runtime\s+pi|runs\.sqlite|secrets\./i); }); - it("documents the project primarily in Chinese and explains the exact CI badge", async () => { + it("documents the project primarily in Chinese and links the exact CI badge without a status legend", async () => { const readme = await readFile("README.md", "utf8"); - const badgeOffset = readme.indexOf(ciBadgeMarkdown); - expect(badgeOffset).toBeGreaterThanOrEqual(0); - - const textAfterBadge = readme.slice(badgeOffset + ciBadgeMarkdown.length); - expect(textAfterBadge).toMatch(/^\n\n> \*\*徽章说明:\*\*/); - for (const explanation of [ - "绿色 / `passing` 表示自动测试与构建通过", - "红色 / `failing` 表示失败", - "点击徽章可查看 GitHub Actions 日志" + expect(readme).toContain(ciBadgeMarkdown); + for (const removedLegendText of [ + "徽章说明", + "绿色 / `passing`", + "红色 / `failing`" ]) { - expect(textAfterBadge.slice(0, 180)).toContain(explanation); + expect(readme).not.toContain(removedLegendText); } for (const passage of [