Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 23 additions & 32 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,31 @@
## Scope 与 Non-goals
## 本次改动

- Scope:<!-- 本 PR 唯一阶段和可验证交付 -->
- Non-goals:<!-- 明确不在本 PR 实现的能力 -->
- 目标与范围:<!-- 本 PR 唯一阶段和可验证交付 -->
- 不包含:<!-- 明确未实施的相关能力 -->

## 兼容、Migration 与 Rollback
## 验证与状态

- [ ] API / Contract / Fingerprint / Snapshot 兼容影响已说明
- [ ] 数据库变更包含 Alembic upgrade/downgrade;无数据库变更时已注明
- [ ] Standalone / Compact / Full 兼容影响已验证或标为不适用
- Rollback:<!-- 回退步骤、数据可逆性与触发条件 -->
- 已执行:<!-- 命令、结果、CI Run;不要粘贴 Secret -->
- 未执行及原因:<!-- 包括完整检查、部署形态或发布验收 -->
- 最新提交的远程合并门禁:<!-- 提交 SHA、必需检查结果;尚未运行时注明 -->
- [ ] 本地定向验证已记录;未将其表述为 CI 或发布验收完成
- [ ] 合并前使用适用的 `ci:light` 或 `ci:milestone`;代码和开发规则改动不用 `ci:light`

## 数据、安全与可观测性
## 风险与后续

- [ ] 新字段已标记数据分类、加密、Rotation、Retention、Export、Support Bundle 与 Redaction
- [ ] 未记录或返回 Password、Authorization、Cookie、Token、Secret、PII 或未脱敏敏感正文
- [ ] 外部错误使用标准错误信封并包含 Trace ID
- [ ] 外部 URL、文件、文档、Workflow/Template 和 Evidence 均按不可信输入处理
- 风险、失败模式及回退:<!-- 说明影响范围、监控和可逆性 -->
- 阻断问题与非阻断跟踪:<!-- 真实 P1/P2 缺陷需处理;独立改进可给出后续记录 -->

## 四维 Review
## 按变更适用性补充

- [ ] 正确性与边界条件
- [ ] Security / Authorization / Tenant 隔离
- [ ] Compatibility / Migration / Rollback
- [ ] Operability / Observability / Documentation
- API / Contract / Fingerprint / Snapshot 兼容影响:<!-- 不适用时注明 -->
- 数据库 migration upgrade/downgrade:<!-- 无数据库变更时注明 -->
- Standalone / Compact / Full 兼容与 Playwright E2E:<!-- 选择适用形态及证据 -->
- Golden / Roundtrip / Failure-path:<!-- 按改动范围 -->
- Backend format / Ruff / mypy / pytest 与 Frontend format / lint / coverage / build:<!-- 说明已运行和未运行项 -->
- 安全、授权、租户隔离和不可信输入:<!-- 按改动范围 -->
- 数据分类、加密、Rotation、Retention、Export、Support Bundle:<!-- 新字段或数据路径适用 -->
- 外部错误信封和 Trace ID:<!-- 外部错误变更适用 -->
- 人工审批与权限范围:<!-- 高风险操作适用 -->

## 验证方式

- [ ] Backend:format、ruff、mypy、pytest
- [ ] Frontend:format、lint、coverage、build
- [ ] Compose Playwright E2E 或说明不适用原因
- [ ] Golden / Roundtrip / Migration / Failure-path(按变更适用)
- [ ] 已选择 `ci:light` 或 `ci:milestone`,最新 Head 的对应远程门禁通过;轻量模式的本地定向验证已记录
- 证据:<!-- 命令、CI Run、截图或 Trace;不要粘贴 Secret -->

## 风险、审批与 Review Thread

- 风险:<!-- 失败模式、影响范围、监控与停止条件 -->
- [ ] 高风险操作具备明确人工审批;本 PR 未扩大既有 Scope/权限
- [ ] 所有 P1/P2 与未解决 Review Thread 已回复证据并关闭
现有授权诊断采集遵循请求级安装/项目策略:默认 OFF 不扫描、替换或阻断已授权值;ON 使用既有脱敏。不扩大采集。Review Thread 以问题是否仍真实存在及证据判定;阻断缺陷须解决,非阻断建议可记录后续跟踪。
25 changes: 21 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,25 @@
- Treat target URLs, uploaded files, imported documents, workflow definitions, and templates as untrusted input.
- Secret values are write-only at API boundaries and encrypted at rest.

## Required checks
## Development workflow

- Backend: `uv run ruff format --check .`, `uv run ruff check .`, `uv run mypy app`, and `uv run pytest`.
- Frontend: `pnpm format:check`, `pnpm lint`, `pnpm test:coverage`, and `pnpm build`.
- End-to-end behavior: Playwright against the Compose stack.
- Identify the affected modules and their tests before editing. Make the smallest change that satisfies the task.
- Keep unrelated refactors, historical warnings, and toolchain upgrades outside the task. Report a separate issue if it does not block the current work.
- Run targeted checks while developing. Do not repeat an unchanged failure without a relevant fix or new diagnostic information. Preserve full logs, but read and report the relevant failure summary first.
- Do not treat a targeted pass as proof that the full CI gate or release acceptance passed.

## Validation scope

- Documentation-only changes: check the changed document's format, links, and required structure. CI governance files and executable documentation examples are not automatically documentation-only changes.
- Local backend changes: run relevant Ruff checks and named backend tests with `make test-backend-targeted TARGETS='["tests/test_imports_api.py"]'`. Run `mypy app` when cross-file typing may be affected.
- Local frontend changes: run named Vitest files with `make test-frontend-targeted TARGETS='["src/lib/api.test.ts"]'`, plus affected formatting, lint, type, or build checks as applicable.
- Core or cross-module changes: expand checks to affected integration paths and deployment shapes. Engine, scheduling, authentication, authorization, tenant isolation, execution snapshots, shared contracts, database models or migrations, dependencies, and deployment changes require broader validation before merge.
- During development, do not default to full coverage, Compose, Windows packaging, historical upgrades, or capacity checks. Run them when the change's risk or the merge/release gate requires them.

`make check` remains the complete local backend and frontend check. It runs backend `uv run ruff format --check .`, `uv run ruff check .`, `uv run mypy app`, and `uv run pytest`; frontend `pnpm format:check`, `pnpm lint`, `pnpm test:coverage`, and `pnpm build`. Complete end-to-end acceptance uses Playwright against the Compose stack when applicable. Targeted backend tests explicitly disable coverage collection; the full backend 90% and frontend 80% coverage thresholds remain in force.

## Completion and reporting

- Distinguish local targeted validation, remote merge gates for the latest commit, and full release acceptance.
- Report changed files, checks actually run, checks not run, and remaining risks. Never infer a green CI gate or release readiness from a local targeted pass.
- Use `ci:light` only for its allowed documentation scope. Code and development-rule changes require the applicable `ci:milestone` or controlled governance path; do not bypass branch protection or required checks.
16 changes: 11 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@
- 提交信息采用 Conventional Commits,例如 `feat(api): add project endpoint`
- 一个 Pull Request 聚焦一个可验证目标

## 提交前检查
## 开发中的定向验证

```bash
make check
make test-backend-targeted TARGETS='["tests/test_imports_api.py"]'
make test-frontend-targeted TARGETS='["src/lib/api.test.ts"]'
```

`TARGETS` 是非空 JSON 路径数组,可传多个真实测试文件;文件名中的空格按一个目标处理。后端定向测试明确使用 `--no-cov`,前端使用普通 Vitest 测试,不运行全量覆盖率。按改动范围补充 Ruff、格式、lint、类型检查或构建。引擎、调度、认证授权、租户隔离、执行快照、共享契约、数据库、依赖与部署变更需要扩大相关集成和部署验证。

完整本地前后端检查仍使用 `make check`。Compose Playwright、跨部署形态、历史升级和发布验收按变更风险及合并/发布门禁执行;不要把每次编辑都当作发布验收。CI 治理调整由独立授权的变更处理,不使用 `ci:light` 规避代码门禁。

涉及数据库模型的变更必须包含 Alembic 迁移;涉及 API 契约的变更必须更新测试和 OpenAPI 示例。

## 完成定义

- 验收标准通过
- 单元/集成测试通过
- 静态检查通过
- 本地定向验证:记录实际命令、结果和未运行的完整检查;仅说明当前改动范围内的结论。
- 可合并:最新提交的远程必需门禁通过,真实阻断缺陷已处理;不能复用旧提交的成功状态。
- 可发布:适用的完整发布验收完成,包括相关部署形态与端到端验证。
- 遵循当前项目脱敏策略:默认 OFF 不自动扫描或改写已授权内容;开启 ON 时不得写入未脱敏的敏感日志
- 文档与变更保持一致
- 独立的非阻断改进可记录后续跟踪;同一失败只在获得新信息或相关修复后重试。保留完整日志,报告先摘取有关的失败信息。
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
.PHONY: help install dev-backend dev-frontend test test-integration lint format check up down logs
.PHONY: help install dev-backend dev-frontend test test-backend-targeted test-frontend-targeted test-integration lint format check up down logs

help:
@echo "make install Install backend and frontend dependencies"
@echo "make dev-backend Run FastAPI development server"
@echo "make dev-frontend Run Vite development server"
@echo "make test-backend-targeted TARGETS='[\"tests/test_imports_api.py\"]'"
@echo "make test-frontend-targeted TARGETS='[\"src/lib/api.test.ts\"]'"
@echo "make check Run backend tests/lint and frontend checks"

install:
Expand All @@ -19,6 +21,12 @@ dev-frontend:
test:
cd backend && uv run pytest

test-backend-targeted:
@python3 scripts/run_targeted_tests.py backend

test-frontend-targeted:
@python3 scripts/run_targeted_tests.py frontend

test-integration:
docker compose up -d postgres redis minio
cd backend && uv run pytest -m integration
Expand Down
82 changes: 82 additions & 0 deletions scripts/run_targeted_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Run only explicitly named backend or frontend test files."""

from __future__ import annotations

import json
import os
import shlex
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
PROJECT_NAMES = ("backend", "frontend")


def valid_test_name(project: str, name: str) -> bool:
if project == "backend":
return name.startswith("test_") and name.endswith(".py")
return name.endswith((".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"))


def test_paths(project: str) -> list[str]:
raw = os.environ.get("TARGETS", "")
try:
targets = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError("TARGETS must be a nonempty JSON array of test paths") from exc
if not isinstance(targets, list) or not targets:
raise ValueError("TARGETS must be a nonempty JSON array of test paths")

project_root = ROOT / project
test_root = project_root / ("tests" if project == "backend" else "src")
paths: list[str] = []
for target in targets:
if not isinstance(target, str) or not target.strip():
raise ValueError("Each target must be a nonempty test file path")
relative = Path(target)
if relative.is_absolute() or ".." in relative.parts:
message = f"Target must stay inside {test_root.relative_to(ROOT)}: {target}"
raise ValueError(message)
resolved = (project_root / relative).resolve()
if not resolved.is_relative_to(test_root.resolve()) or not resolved.is_file():
raise ValueError(f"Target must be an existing test file: {target}")
if not valid_test_name(project, resolved.name):
raise ValueError(f"Target must be a test file: {target}")
paths.append(str(relative))
return paths


def main() -> int:
valid_arguments = (
len(sys.argv) in (2, 3)
and sys.argv[1] in PROJECT_NAMES
and (len(sys.argv) == 2 or sys.argv[2] == "--dry-run")
)
if not valid_arguments:
print(
"Usage: run_targeted_tests.py {backend|frontend} [--dry-run]",
file=sys.stderr,
)
return 2
project = sys.argv[1]
try:
paths = test_paths(project)
except ValueError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 2
command = (
["uv", "run", "pytest", "--no-cov", *paths]
if project == "backend"
else ["pnpm", "test", *paths]
)
project_root = ROOT / project
print(f"In {project_root}: {shlex.join(command)}", flush=True)
if len(sys.argv) == 3:
return 0
# The command prefix is fixed; every file path was resolved inside the test root.
return subprocess.run(command, cwd=project_root, check=False).returncode # noqa: S603


if __name__ == "__main__":
raise SystemExit(main())
Loading