From ee622915510947146c90fe2974e5b7beb3a80e62 Mon Sep 17 00:00:00 2001 From: yaojingang Date: Tue, 16 Jun 2026 22:02:30 +0800 Subject: [PATCH] Extract trust check script inventory helpers --- reports/architecture_maintainability.json | 22 ++-- reports/architecture_maintainability.md | 10 +- reports/context_budget.json | 20 +-- reports/context_budget.md | 2 +- reports/context_budget_summary.json | 10 +- reports/python_compatibility.json | 8 +- reports/python_compatibility.md | 2 +- reports/security_trust_report.json | 30 ++++- reports/security_trust_report.md | 11 +- scripts/trust_check.py | 136 +------------------- scripts/trust_check_scripts.py | 146 ++++++++++++++++++++++ tests/verify_trust_check.py | 2 + 12 files changed, 220 insertions(+), 179 deletions(-) create mode 100644 scripts/trust_check_scripts.py diff --git a/reports/architecture_maintainability.json b/reports/architecture_maintainability.json index 4edf7a92..d1347d1c 100644 --- a/reports/architecture_maintainability.json +++ b/reports/architecture_maintainability.json @@ -4,31 +4,24 @@ "generated_at": "2026-06-16", "skill_dir": ".", "summary": { - "python_file_count": 204, - "script_file_count": 131, + "python_file_count": 205, + "script_file_count": 132, "test_file_count": 73, "internal_module_count": 48, - "cli_script_count": 85, + "cli_script_count": 86, "command_handler_count": 68, "entrypoint_command_handler_count": 18, "command_module_count": 6, "warn_line_threshold": 900, "watch_line_threshold": 720, "block_line_threshold": 1500, - "largest_file_lines": 714, + "largest_file_lines": 707, "watchlist_count": 0, "hotspot_count": 0, "blocker_count": 0, "decision": "pass" }, "largest_files": [ - { - "path": "scripts/trust_check.py", - "lines": 714, - "kind": "internal-module", - "severity": "pass", - "recommendation": "Watch this file before adding new responsibilities; extract a helper module when one concern dominates." - }, { "path": "scripts/review_studio_gates.py", "lines": 707, @@ -105,6 +98,13 @@ "kind": "cli-script", "severity": "pass", "recommendation": "Watch this file before adding new responsibilities; extract a helper module when one concern dominates." + }, + { + "path": "scripts/build_skill_atlas.py", + "lines": 637, + "kind": "cli-script", + "severity": "pass", + "recommendation": "Watch this file before adding new responsibilities; extract a helper module when one concern dominates." } ], "watchlist": [], diff --git a/reports/architecture_maintainability.md b/reports/architecture_maintainability.md index 97ca201f..7dd67cbd 100644 --- a/reports/architecture_maintainability.md +++ b/reports/architecture_maintainability.md @@ -5,15 +5,15 @@ Generated at: `2026-06-16` ## Summary - decision: `pass` -- python files: `204` -- scripts: `131` +- python files: `205` +- scripts: `132` - tests: `73` - internal modules: `48` -- CLI scripts: `85` +- CLI scripts: `86` - Yao CLI command handlers: `68` - entrypoint command handlers: `18` - command modules: `6` -- largest file lines: `714` +- largest file lines: `707` - watch threshold lines: `720` - watchlist: `0` - hotspots: `0` @@ -33,7 +33,6 @@ No near-threshold files found. | File | Lines | Kind | Severity | | --- | ---: | --- | --- | -| `scripts/trust_check.py` | `714` | `internal-module` | `pass` | | `scripts/review_studio_gates.py` | `707` | `internal-module` | `pass` | | `scripts/apply_adaptation.py` | `706` | `cli-script` | `pass` | | `tests/verify_yao_cli.py` | `696` | `test` | `pass` | @@ -45,6 +44,7 @@ No near-threshold files found. | `scripts/render_review_studio.py` | `647` | `cli-script` | `pass` | | `scripts/render_reference_synthesis.py` | `644` | `cli-script` | `pass` | | `scripts/cross_packager.py` | `638` | `cli-script` | `pass` | +| `scripts/build_skill_atlas.py` | `637` | `cli-script` | `pass` | ## Release Rule diff --git a/reports/context_budget.json b/reports/context_budget.json index 62614502..6249c54d 100644 --- a/reports/context_budget.json +++ b/reports/context_budget.json @@ -6,16 +6,16 @@ "context_budget_tier": "production", "context_budget_limit": 1000, "skill_body_tokens": 797, - "other_text_tokens": 1051797, + "other_text_tokens": 1052134, "estimated_initial_load_tokens": 990, - "estimated_total_text_tokens": 1052594, - "deferred_resource_tokens": 487270, + "estimated_total_text_tokens": 1052931, + "deferred_resource_tokens": 487365, "deferred_resource_warn_threshold": 120000, "deferred_resource_dirs": [ { "path": "scripts", - "estimated_tokens": 427172, - "file_count": 131 + "estimated_tokens": 427267, + "file_count": 132 }, { "path": "references", @@ -36,8 +36,8 @@ "large_deferred_resource_dirs": [ { "path": "scripts", - "estimated_tokens": 427172, - "file_count": 131 + "estimated_tokens": 427267, + "file_count": 132 } ], "deferred_resource_governance": { @@ -59,14 +59,14 @@ ], "missing": [], "path": "scripts", - "estimated_tokens": 427172, - "file_count": 131, + "estimated_tokens": 427267, + "file_count": 132, "rationale": "Script resources are deterministic deferred tools, not initial-load prompt context." } ], "summary": "Large deferred resources are indexed and backed by evidence." }, - "relevant_file_count": 654, + "relevant_file_count": 656, "unused_resource_dirs": [], "quality_signal_points": 130, "quality_density": 131.3 diff --git a/reports/context_budget.md b/reports/context_budget.md index 5ed54247..402a30ae 100644 --- a/reports/context_budget.md +++ b/reports/context_budget.md @@ -2,7 +2,7 @@ | Target | Path | Tier | Limit | Initial | SKILL | Deferred | Resource Governance | Large Deferred Dirs | Quality Density | Unused Dirs | Status | | --- | --- | --- | ---: | ---: | ---: | ---: | --- | --- | ---: | --- | --- | -| root | `.` | `production` | 1000 | 990 | 797 | 487270 | `governed` | scripts:427172 | 131.3 | - | ok | +| root | `.` | `production` | 1000 | 990 | 797 | 487365 | `governed` | scripts:427267 | 131.3 | - | ok | | complex-release-orchestrator | `examples/complex-release-orchestrator/generated-skill` | `production` | 1000 | 790 | 718 | 1657 | `not-required` | - | 164.6 | - | ok | | governed-incident-command | `examples/governed-incident-command/generated-skill` | `production` | 1000 | 760 | 658 | 1030 | `not-required` | - | 171.1 | - | ok | diff --git a/reports/context_budget_summary.json b/reports/context_budget_summary.json index e7f95811..fcbf1ec7 100644 --- a/reports/context_budget_summary.json +++ b/reports/context_budget_summary.json @@ -8,12 +8,12 @@ "budget_limit": 1000, "initial_tokens": 990, "skill_body_tokens": 797, - "deferred_resource_tokens": 487270, + "deferred_resource_tokens": 487365, "large_deferred_resource_dirs": [ { "path": "scripts", - "estimated_tokens": 427172, - "file_count": 131 + "estimated_tokens": 427267, + "file_count": 132 } ], "deferred_resource_governance": { @@ -35,8 +35,8 @@ ], "missing": [], "path": "scripts", - "estimated_tokens": 427172, - "file_count": 131, + "estimated_tokens": 427267, + "file_count": 132, "rationale": "Script resources are deterministic deferred tools, not initial-load prompt context." } ], diff --git a/reports/python_compatibility.json b/reports/python_compatibility.json index 47b7fb5d..90ba9b52 100644 --- a/reports/python_compatibility.json +++ b/reports/python_compatibility.json @@ -5,7 +5,7 @@ "root": ".", "summary": { "target_python": "3.11", - "file_count": 207, + "file_count": 208, "issue_count": 0, "syntax_error_count": 0, "fstring_311_violation_count": 0, @@ -706,6 +706,12 @@ "issue_count": 0, "issues": [] }, + { + "path": "scripts/trust_check_scripts.py", + "ok": true, + "issue_count": 0, + "issues": [] + }, { "path": "scripts/upgrade_check.py", "ok": true, diff --git a/reports/python_compatibility.md b/reports/python_compatibility.md index 4b0f16ed..a744a0a6 100644 --- a/reports/python_compatibility.md +++ b/reports/python_compatibility.md @@ -6,7 +6,7 @@ Generated at: `2026-06-16` - decision: `pass` - target python: `3.11` -- files scanned: `207` +- files scanned: `208` - issues: `0` - syntax errors: `0` - f-string 3.11 violations: `0` diff --git a/reports/security_trust_report.json b/reports/security_trust_report.json index 453af26f..e9b4e43b 100644 --- a/reports/security_trust_report.json +++ b/reports/security_trust_report.json @@ -2,9 +2,9 @@ "ok": true, "skill_dir": ".", "summary": { - "scanned_files": 222, - "script_count": 131, - "internal_module_count": 45, + "scanned_files": 223, + "script_count": 132, + "internal_module_count": 46, "secret_findings": 0, "dependency_files": [ "requirements-ci.txt" @@ -22,8 +22,8 @@ "help_smoke_failed_count": 0, "interactive_script_count": 0, "package_hash_scope": "source-contract-without-generated-reports", - "package_hash_file_count": 222, - "package_sha256": "a163b6e288c5342e683875b47fdfe62989d3f09241c0c8fd86ae615c6d5da7ad" + "package_hash_file_count": 223, + "package_sha256": "9b29a4cc28ab3fb1df2434b22e26b9e92ee04c096f35da350705c4c92307260e" }, "failures": [], "warnings": [], @@ -1600,6 +1600,20 @@ "network_urls": [], "network_hosts": [] }, + { + "path": "scripts/trust_check_scripts.py", + "interface": "internal-module", + "interface_declared": true, + "interface_reason": "Static script inventory helpers imported by trust_check.py.", + "has_argparse": true, + "has_main_guard": true, + "uses_input": false, + "uses_network": false, + "uses_file_write": false, + "uses_subprocess": false, + "network_urls": [], + "network_hosts": [] + }, { "path": "scripts/upgrade_check.py", "interface": "cli", @@ -1915,7 +1929,7 @@ "checked_count": 86, "passed_count": 86, "failed_count": 0, - "skipped_count": 45, + "skipped_count": 46, "failed_scripts": [], "results": [ { @@ -2896,6 +2910,10 @@ "path": "scripts/skillops_opportunity.py", "reason": "internal module" }, + { + "path": "scripts/trust_check_scripts.py", + "reason": "internal module" + }, { "path": "scripts/world_class_evidence_contract.py", "reason": "internal module" diff --git a/reports/security_trust_report.md b/reports/security_trust_report.md index f1fb89d9..b1434882 100644 --- a/reports/security_trust_report.md +++ b/reports/security_trust_report.md @@ -1,9 +1,9 @@ # Security Trust Report - OK: `True` -- Scanned files: `222` -- Scripts: `131` -- Internal script modules: `45` +- Scanned files: `223` +- Scripts: `132` +- Internal script modules: `46` - Secret findings: `0` - Network-capable scripts: `3` - Network policy covered scripts: `3` @@ -15,8 +15,8 @@ - CLI help smoke failures: `0` - Interactive scripts: `0` - Package hash scope: `source-contract-without-generated-reports` -- Package hash files: `222` -- Package SHA256: `a163b6e288c5342e683875b47fdfe62989d3f09241c0c8fd86ae615c6d5da7ad` +- Package hash files: `223` +- Package SHA256: `9b29a4cc28ab3fb1df2434b22e26b9e92ee04c096f35da350705c4c92307260e` ## Failures @@ -173,6 +173,7 @@ | scripts/telemetry_native_host.py | cli | False | True | True | False | False | True | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. | | scripts/trigger_eval.py | cli | False | True | True | False | False | False | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. | | scripts/trust_check.py | cli | False | True | True | False | False | True | True | Default CLI classification; add SCRIPT_INTERFACE for internal modules. | +| scripts/trust_check_scripts.py | internal-module | True | True | True | False | False | False | False | Static script inventory helpers imported by trust_check.py. | | scripts/upgrade_check.py | cli | False | True | True | False | False | True | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. | | scripts/validate_skill.py | cli | False | True | True | False | False | False | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. | | scripts/verify_package.py | cli | False | True | True | False | False | True | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. | diff --git a/scripts/trust_check.py b/scripts/trust_check.py index 2d505d82..8d03125c 100644 --- a/scripts/trust_check.py +++ b/scripts/trust_check.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import argparse -import ast import hashlib import json import subprocess @@ -9,20 +8,20 @@ import re from datetime import date from pathlib import Path from typing import Any -from urllib.parse import urlparse try: import yaml except ImportError: # pragma: no cover yaml = None +from trust_check_scripts import INTERNAL_SCRIPT_INTERFACE, script_inventory + ROOT = Path(__file__).resolve().parent.parent SCAN_DIRS = ["agents", "assets", "docs", "evals", "references", "runtime", "scripts", "security", "skill-ir", "templates"] ROOT_FILES = ["SKILL.md", "README.md", "manifest.json", "requirements-ci.txt", "Makefile"] TEXT_SUFFIXES = {".css", ".js", ".md", ".json", ".jsonl", ".yaml", ".yml", ".py", ".sh", ".txt", ".toml"} PACKAGE_HASH_SCOPE = "source-contract-without-generated-reports" -INTERNAL_SCRIPT_INTERFACE = "internal-module" NETWORK_POLICY_REL_PATH = "security/network_policy.json" PERMISSION_POLICY_REL_PATH = "security/permission_policy.json" HELP_SMOKE_TIMEOUT_SECONDS = 5.0 @@ -94,137 +93,6 @@ def scan_secrets(skill_dir: Path, files: list[Path]) -> list[dict[str, Any]]: return findings -def script_inventory(skill_dir: Path) -> list[dict[str, Any]]: - scripts_dir = skill_dir / "scripts" - if not scripts_dir.exists(): - return [] - inventory = [] - for path in sorted(scripts_dir.glob("*.py")): - text = path.read_text(encoding="utf-8", errors="replace") - flags = script_flags(text) - interface = script_interface(text) - urls = extract_url_literals(text) - inventory.append( - { - "path": relpath(skill_dir, path), - "interface": interface["name"], - "interface_declared": interface["declared"], - "interface_reason": interface["reason"], - "has_argparse": "argparse" in text, - "has_main_guard": 'if __name__ == "__main__"' in text, - "uses_input": flags["uses_input"], - "uses_network": flags["uses_network"], - "uses_file_write": flags["uses_file_write"], - "uses_subprocess": flags["uses_subprocess"], - "network_urls": urls, - "network_hosts": sorted({urlparse(url).hostname or "" for url in urls if urlparse(url).hostname}), - } - ) - return inventory - - -def string_assignment(tree: ast.Module, variable_name: str) -> str: - for node in tree.body: - value_node = None - if isinstance(node, ast.Assign): - if any(isinstance(target, ast.Name) and target.id == variable_name for target in node.targets): - value_node = node.value - elif isinstance(node, ast.AnnAssign): - target = node.target - if isinstance(target, ast.Name) and target.id == variable_name: - value_node = node.value - if isinstance(value_node, ast.Constant) and isinstance(value_node.value, str): - return value_node.value - return "" - - -def script_interface(text: str) -> dict[str, Any]: - try: - tree = ast.parse(text) - except SyntaxError: - match = re.search(r"SCRIPT_INTERFACE\s*=\s*['\"]([^'\"]+)['\"]", text) - reason_match = re.search(r"SCRIPT_INTERFACE_REASON\s*=\s*['\"]([^'\"]+)['\"]", text) - name = match.group(1) if match else "cli" - reason = reason_match.group(1) if reason_match else "" - return {"name": name, "declared": bool(match), "reason": reason} - - name = string_assignment(tree, "SCRIPT_INTERFACE") - reason = string_assignment(tree, "SCRIPT_INTERFACE_REASON") - if name: - return {"name": name, "declared": True, "reason": reason} - return {"name": "cli", "declared": False, "reason": "Default CLI classification; add SCRIPT_INTERFACE for internal modules."} - - -def extract_url_literals(text: str) -> list[str]: - values: list[str] = [] - try: - tree = ast.parse(text) - for node in ast.walk(tree): - if isinstance(node, ast.Constant) and isinstance(node.value, str): - values.append(node.value) - except SyntaxError: - values = re.findall(r"['\"]([^'\"]+)['\"]", text) - - urls = [] - seen = set() - for value in values: - for match in re.finditer(r"https?://[^\s'\"<>]+", value): - url = match.group(0).rstrip(").,]") - if url not in seen: - seen.add(url) - urls.append(url) - return urls - - -def call_name(node: ast.AST) -> str: - if isinstance(node, ast.Name): - return node.id - if isinstance(node, ast.Attribute): - base = call_name(node.value) - return f"{base}.{node.attr}" if base else node.attr - return "" - - -def script_flags(text: str) -> dict[str, bool]: - try: - tree = ast.parse(text) - except SyntaxError: - return { - "uses_input": bool(re.search(r"\b(?:input|getpass)\s*\(", text)), - "uses_network": bool(re.search(r"\b(?:urlopen|Request)\s*\(|\brequests\.", text)), - "uses_file_write": bool( - re.search(r"\.(?:write_text|write_bytes|mkdir|unlink)\s*\(", text) - or re.search(r"\b(?:open)\s*\([^)]*['\"][wa+x]", text) - or "shutil.rmtree" in text - ), - "uses_subprocess": "subprocess." in text, - } - call_nodes = [node for node in ast.walk(tree) if isinstance(node, ast.Call)] - calls = [call_name(node.func) for node in call_nodes] - return { - "uses_input": any(name in {"input", "getpass.getpass"} or name.endswith(".getpass") for name in calls), - "uses_network": any(name in {"urlopen", "Request"} or name.startswith("requests.") for name in calls), - "uses_file_write": any(is_file_write_call(node, call_name(node.func)) for node in call_nodes), - "uses_subprocess": any(name.startswith("subprocess.") for name in calls), - } - - -def is_file_write_call(node: ast.Call, name: str) -> bool: - if name.endswith((".write_text", ".write_bytes", ".mkdir", ".unlink", ".rmdir")): - return True - if name in {"shutil.copy", "shutil.copy2", "shutil.copytree", "shutil.move", "shutil.rmtree"}: - return True - if name == "zipfile.ZipFile": - return True - if name in {"open", "Path.open"} or name.endswith(".open"): - args = list(node.args) - keywords = {keyword.arg: keyword.value for keyword in node.keywords if keyword.arg} - mode_node = args[1] if len(args) > 1 else keywords.get("mode") - if isinstance(mode_node, ast.Constant) and isinstance(mode_node.value, str): - return any(flag in mode_node.value for flag in ("w", "a", "x", "+")) - return False - - def dependency_status(skill_dir: Path) -> dict[str, Any]: candidates = ["requirements-ci.txt", "requirements.txt", "pyproject.toml", "package-lock.json", "uv.lock", "poetry.lock"] present = [name for name in candidates if (skill_dir / name).exists()] diff --git a/scripts/trust_check_scripts.py b/scripts/trust_check_scripts.py new file mode 100644 index 00000000..44cdd7f5 --- /dev/null +++ b/scripts/trust_check_scripts.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +import ast +import re +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + + +SCRIPT_INTERFACE = "internal-module" +SCRIPT_INTERFACE_REASON = "Static script inventory helpers imported by trust_check.py." +INTERNAL_SCRIPT_INTERFACE = "internal-module" + + +def relpath(skill_dir: Path, path: Path) -> str: + return str(path.relative_to(skill_dir)) + + +def script_inventory(skill_dir: Path) -> list[dict[str, Any]]: + scripts_dir = skill_dir / "scripts" + if not scripts_dir.exists(): + return [] + inventory = [] + for path in sorted(scripts_dir.glob("*.py")): + text = path.read_text(encoding="utf-8", errors="replace") + flags = script_flags(text) + interface = script_interface(text) + urls = extract_url_literals(text) + inventory.append( + { + "path": relpath(skill_dir, path), + "interface": interface["name"], + "interface_declared": interface["declared"], + "interface_reason": interface["reason"], + "has_argparse": "argparse" in text, + "has_main_guard": 'if __name__ == "__main__"' in text, + "uses_input": flags["uses_input"], + "uses_network": flags["uses_network"], + "uses_file_write": flags["uses_file_write"], + "uses_subprocess": flags["uses_subprocess"], + "network_urls": urls, + "network_hosts": sorted({urlparse(url).hostname or "" for url in urls if urlparse(url).hostname}), + } + ) + return inventory + + +def string_assignment(tree: ast.Module, variable_name: str) -> str: + for node in tree.body: + value_node = None + if isinstance(node, ast.Assign): + if any(isinstance(target, ast.Name) and target.id == variable_name for target in node.targets): + value_node = node.value + elif isinstance(node, ast.AnnAssign): + target = node.target + if isinstance(target, ast.Name) and target.id == variable_name: + value_node = node.value + if isinstance(value_node, ast.Constant) and isinstance(value_node.value, str): + return value_node.value + return "" + + +def script_interface(text: str) -> dict[str, Any]: + try: + tree = ast.parse(text) + except SyntaxError: + match = re.search(r"SCRIPT_INTERFACE\s*=\s*['\"]([^'\"]+)['\"]", text) + reason_match = re.search(r"SCRIPT_INTERFACE_REASON\s*=\s*['\"]([^'\"]+)['\"]", text) + name = match.group(1) if match else "cli" + reason = reason_match.group(1) if reason_match else "" + return {"name": name, "declared": bool(match), "reason": reason} + + name = string_assignment(tree, "SCRIPT_INTERFACE") + reason = string_assignment(tree, "SCRIPT_INTERFACE_REASON") + if name: + return {"name": name, "declared": True, "reason": reason} + return {"name": "cli", "declared": False, "reason": "Default CLI classification; add SCRIPT_INTERFACE for internal modules."} + + +def extract_url_literals(text: str) -> list[str]: + values: list[str] = [] + try: + tree = ast.parse(text) + for node in ast.walk(tree): + if isinstance(node, ast.Constant) and isinstance(node.value, str): + values.append(node.value) + except SyntaxError: + values = re.findall(r"['\"]([^'\"]+)['\"]", text) + + urls = [] + seen = set() + for value in values: + for match in re.finditer(r"https?://[^\s'\"<>]+", value): + url = match.group(0).rstrip(").,]") + if url not in seen: + seen.add(url) + urls.append(url) + return urls + + +def call_name(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + base = call_name(node.value) + return f"{base}.{node.attr}" if base else node.attr + return "" + + +def script_flags(text: str) -> dict[str, bool]: + try: + tree = ast.parse(text) + except SyntaxError: + return { + "uses_input": bool(re.search(r"\b(?:input|getpass)\s*\(", text)), + "uses_network": bool(re.search(r"\b(?:urlopen|Request)\s*\(|\brequests\.", text)), + "uses_file_write": bool( + re.search(r"\.(?:write_text|write_bytes|mkdir|unlink)\s*\(", text) + or re.search(r"\b(?:open)\s*\([^)]*['\"][wa+x]", text) + or "shutil.rmtree" in text + ), + "uses_subprocess": "subprocess." in text, + } + call_nodes = [node for node in ast.walk(tree) if isinstance(node, ast.Call)] + calls = [call_name(node.func) for node in call_nodes] + return { + "uses_input": any(name in {"input", "getpass.getpass"} or name.endswith(".getpass") for name in calls), + "uses_network": any(name in {"urlopen", "Request"} or name.startswith("requests.") for name in calls), + "uses_file_write": any(is_file_write_call(node, call_name(node.func)) for node in call_nodes), + "uses_subprocess": any(name.startswith("subprocess.") for name in calls), + } + + +def is_file_write_call(node: ast.Call, name: str) -> bool: + if name.endswith((".write_text", ".write_bytes", ".mkdir", ".unlink", ".rmdir")): + return True + if name in {"shutil.copy", "shutil.copy2", "shutil.copytree", "shutil.move", "shutil.rmtree"}: + return True + if name == "zipfile.ZipFile": + return True + if name in {"open", "Path.open"} or name.endswith(".open"): + args = list(node.args) + keywords = {keyword.arg: keyword.value for keyword in node.keywords if keyword.arg} + mode_node = args[1] if len(args) > 1 else keywords.get("mode") + if isinstance(mode_node, ast.Constant) and isinstance(mode_node.value, str): + return any(flag in mode_node.value for flag in ("w", "a", "x", "+")) + return False diff --git a/tests/verify_trust_check.py b/tests/verify_trust_check.py index 84fae59d..3e8d0990 100644 --- a/tests/verify_trust_check.py +++ b/tests/verify_trust_check.py @@ -101,6 +101,7 @@ def main() -> None: "scripts/review_studio_formatting.py", "scripts/review_studio_gates.py", "scripts/review_studio_layout.py", + "scripts/trust_check_scripts.py", "scripts/evidence_consistency_core.py", "scripts/evidence_consistency_release.py", "scripts/evidence_consistency_skill_os2_review.py", @@ -135,6 +136,7 @@ def main() -> None: assert "review_studio_formatting.py" not in warning_text, payload["warnings"] assert "review_studio_gates.py" not in warning_text, payload["warnings"] assert "review_studio_layout.py" not in warning_text, payload["warnings"] + assert "trust_check_scripts.py" not in warning_text, payload["warnings"] assert "evidence_consistency_core.py" not in warning_text, payload["warnings"] assert "evidence_consistency_release.py" not in warning_text, payload["warnings"] assert "evidence_consistency_skill_os2_review.py" not in warning_text, payload["warnings"]