chore(P14-8): 完成运营化与验收收口

这个提交包含在:
2026-03-06 23:24:16 +08:00
父节点 b50b084100
当前提交 d8fff0ba21
共修改 28 个文件,包含 2820 行新增和 123 行删除
+63
查看文件
@@ -0,0 +1,63 @@
const fs = require('fs');
const path = require('path');
const summaryPath = path.resolve(__dirname, '../server/coverage/coverage-summary.json');
if (!fs.existsSync(summaryPath)) {
console.error("No coverage summary found. Run 'npm run test:coverage' first.");
process.exit(1);
}
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
// 初始阈值仅关键模块 lines>=70%,全局先告警
const KEY_MODULES = [
"src/services/featureFlags",
"src/services/verification",
"src/services/integrations/dispatcher.js",
"src/services/integrations/outbox.js",
"src/services/integrations/erp.js",
"src/utils/workflow.js"
];
let allPassed = true;
const messages = [];
for (const [file, stats] of Object.entries(summary)) {
if (file === "total") continue;
const isKeyModule = KEY_MODULES.some(key => file.includes(key));
if (isKeyModule) {
const linesCov = stats.lines.pct;
if (linesCov < 70) {
allPassed = false;
messages.push(`FAIL: ${file} lines coverage is ${linesCov}%, which is below the 70% threshold.`);
} else {
messages.push(`PASS: ${file} lines coverage is ${linesCov}% (>= 70%).`);
}
}
}
const globalLines = summary.total?.lines?.pct ?? 0;
messages.push(`Global lines coverage is ${globalLines}%. (Warn-only)`);
console.log("=== Coverage Gate Report ===");
console.log(messages.join("\n"));
if (process.env.GITHUB_STEP_SUMMARY) {
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `\n## Coverage Gate\n\n\`\`\`text\n${messages.join("\n")}\n\`\`\`\n`);
}
const isPR = process.env.GITHUB_EVENT_NAME === "pull_request";
if (!allPassed) {
if (isPR) {
console.log("WARN: Coverage is below threshold. PRs will not be blocked currently, but please fix it.");
process.exit(0);
} else {
console.error("ERROR: Coverage is below threshold and the branch is NOT a PR. Failing the gate.");
process.exit(1);
}
} else {
console.log("Coverage gate passed successfully.");
process.exit(0);
}
+34
查看文件
@@ -0,0 +1,34 @@
import json
import sys
def main():
if len(sys.argv) > 1:
openapi_path = sys.argv[1]
else:
openapi_path = '/tmp/openapi.json'
try:
with open(openapi_path, 'r') as f:
spec = json.load(f)
except Exception as e:
print(f"Failed to load {openapi_path}: {e}")
sys.exit(1)
paths = spec.get('paths', {})
expected_prefixes = ['/api/reimbursements', '/api/workflow/tasks', '/api/invoice-pool']
missing_prefixes = []
for prefix in expected_prefixes:
matched = any(p.startswith(prefix) for p in paths.keys())
if not matched:
missing_prefixes.append(prefix)
if missing_prefixes:
print(f"ERROR: Missing expected root paths in OpenAPI schema: {missing_prefixes}")
sys.exit(1)
print(f"SUCCESS: Drift check passed. Found {len(paths)} paths.")
if __name__ == '__main__':
main()
+1
查看文件
@@ -25,6 +25,7 @@ CSV_FACT_SOURCES = [
ROOT / "issues" / "2026-02-10_10-35-30-p10-production-hardening.csv",
ROOT / "issues" / "2026-02-11-p11-p12-governance-mobile.csv",
ROOT / "issues" / "2026-02-12_01-09-41-p13-l5-governance.csv",
ROOT / "issues" / "2026-02-13_00-11-56-p14-l5-operationalization.csv",
]
ID_PATTERN = re.compile(r"^[A-Z][0-9][0-9A-Za-z-]*$")
+80
查看文件
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
import sys
import json
import logging
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
def validate_step(step, idx):
required_keys = {
"id": str,
"name": str,
"enabled": bool,
"status": str,
"exit_code": int,
"duration_ms": int,
"started_at": str,
"ended_at": str,
"command": str
}
for key, expected_type in required_keys.items():
if key not in step:
raise ValueError(f"Step[{idx}] missing required key: '{key}'")
if not isinstance(step[key], expected_type):
raise ValueError(f"Step[{idx}] '{key}' must be {expected_type.__name__}, got {type(step[key]).__name__}")
def validate_report(report_path: Path):
if not report_path.exists():
raise FileNotFoundError(f"Report file not found: {report_path}")
try:
report = json.loads(report_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON format: {e}")
# Root Level
if "schema_version" not in report or report["schema_version"] != "1.0":
raise ValueError("Invalid or missing schema_version. Expected '1.0'")
if "generated_at" not in report or not isinstance(report["generated_at"], str):
raise ValueError("Missing or invalid 'generated_at' string")
# Git
if "git" not in report or not isinstance(report["git"], dict):
raise ValueError("Missing or invalid 'git' object")
if "sha" not in report["git"] or not isinstance(report["git"]["sha"], str):
raise ValueError("Missing or invalid 'git.sha'")
if "branch" not in report["git"] or not isinstance(report["git"]["branch"], str):
raise ValueError("Missing or invalid 'git.branch'")
# Overall
if "overall" not in report or not isinstance(report["overall"], dict):
raise ValueError("Missing or invalid 'overall' object")
for key in ["duration_ms", "passed", "failed", "skipped", "total"]:
if key not in report["overall"] or not isinstance(report["overall"][key], int):
raise ValueError(f"Missing or invalid integer 'overall.{key}'")
if "status" not in report["overall"] or not isinstance(report["overall"]["status"], str):
raise ValueError("Missing or invalid string 'overall.status'")
# Steps
if "steps" not in report or not isinstance(report["steps"], list):
raise ValueError("Missing or invalid 'steps' array")
for idx, step in enumerate(report["steps"]):
validate_step(step, idx)
logging.info(f"Report {report_path} passed schema validation (v1.0).")
if __name__ == '__main__':
if len(sys.argv) < 2:
logging.error("Usage: validate_regression_report.py <path/to/report.json>")
sys.exit(1)
try:
validate_report(Path(sys.argv[1]))
except Exception as e:
logging.error(f"Validation failed: {e}")
sys.exit(1)