modify framework
This commit is contained in:
@@ -1,14 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
CHECKS = ("lint", "build", "test")
|
|
||||||
|
|
||||||
|
|
||||||
def _repo_root(cwd: Path) -> Path:
|
def _repo_root(cwd: Path) -> Path:
|
||||||
try:
|
try:
|
||||||
root = subprocess.check_output(
|
root = subprocess.check_output(
|
||||||
@@ -22,25 +18,11 @@ def _repo_root(cwd: Path) -> Path:
|
|||||||
return Path(root)
|
return Path(root)
|
||||||
|
|
||||||
|
|
||||||
def _load_scripts(root: Path) -> dict[str, str]:
|
|
||||||
package_json = root / "package.json"
|
|
||||||
if not package_json.exists():
|
|
||||||
return {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
package = json.loads(package_json.read_text(encoding="utf-8"))
|
|
||||||
except json.JSONDecodeError as exc:
|
|
||||||
_deny(f"Invalid package.json: {exc}")
|
|
||||||
raise SystemExit(0) from exc
|
|
||||||
|
|
||||||
scripts = package.get("scripts", {})
|
|
||||||
if not isinstance(scripts, dict):
|
|
||||||
return {}
|
|
||||||
return {str(name): str(command) for name, command in scripts.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def _is_git_commit(command: str) -> bool:
|
def _is_git_commit(command: str) -> bool:
|
||||||
return re.search(r"\bgit(?:\s+(?:-[A-Za-z]\s+\S+|--[A-Za-z0-9-]+(?:=\S+)?))*\s+commit\b", command) is not None
|
return re.search(
|
||||||
|
r"^\s*git(?:\s+(?:-[A-Za-z]\s+\S+|--[A-Za-z0-9-]+(?:=\S+)?))*\s+commit\b",
|
||||||
|
command,
|
||||||
|
) is not None
|
||||||
|
|
||||||
|
|
||||||
def _deny(reason: str) -> None:
|
def _deny(reason: str) -> None:
|
||||||
@@ -64,26 +46,22 @@ def _tail(text: str, limit: int = 1200) -> str:
|
|||||||
return text[-limit:]
|
return text[-limit:]
|
||||||
|
|
||||||
|
|
||||||
def _run_checks(root: Path, scripts: dict[str, str]) -> str | None:
|
def _build_pre_commit_commands(root: Path) -> list[list[str]]:
|
||||||
npm = shutil.which("npm") or shutil.which("npm.cmd")
|
return [
|
||||||
if npm is None:
|
[sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"],
|
||||||
return "npm was not found, so pre-commit checks could not run."
|
[sys.executable, "scripts/validate_workspace.py"],
|
||||||
|
]
|
||||||
|
|
||||||
for check in CHECKS:
|
|
||||||
if check not in scripts:
|
|
||||||
continue
|
|
||||||
result = subprocess.run(
|
|
||||||
[npm, "run", check],
|
|
||||||
cwd=root,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def _run_checks(root: Path) -> str | None:
|
||||||
|
for command in _build_pre_commit_commands(root):
|
||||||
|
result = subprocess.run(command, cwd=root, capture_output=True, text=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
details = _tail(result.stdout + "\n" + result.stderr)
|
details = _tail(result.stdout + "\n" + result.stderr)
|
||||||
|
label = " ".join(command)
|
||||||
if details:
|
if details:
|
||||||
return f"npm run {check} failed:\n{details}"
|
return f"{label} failed:\n{details}"
|
||||||
return f"npm run {check} failed with exit code {result.returncode}."
|
return f"{label} failed with exit code {result.returncode}."
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -100,7 +78,7 @@ def main() -> int:
|
|||||||
|
|
||||||
cwd = Path(payload.get("cwd") or Path.cwd())
|
cwd = Path(payload.get("cwd") or Path.cwd())
|
||||||
root = _repo_root(cwd)
|
root = _repo_root(cwd)
|
||||||
failure = _run_checks(root, _load_scripts(root))
|
failure = _run_checks(root)
|
||||||
if failure:
|
if failure:
|
||||||
_deny(f"PRE-COMMIT CHECKS: {failure}")
|
_deny(f"PRE-COMMIT CHECKS: {failure}")
|
||||||
|
|
||||||
|
|||||||
+67
-51
@@ -4,19 +4,9 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
SOURCE_SUFFIXES = {".ts", ".tsx", ".js", ".jsx"}
|
SOURCE_SUFFIXES = {".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx", ".ixx"}
|
||||||
TEST_SUFFIXES = ("ts", "tsx", "js", "jsx")
|
TEST_SUFFIXES = {".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx", ".ixx"}
|
||||||
CONFIG_SUFFIXES = {".json", ".css", ".scss", ".md", ".yml", ".yaml"}
|
CONFIG_SUFFIXES = {".json", ".md", ".yml", ".yaml", ".txt", ".cmake"}
|
||||||
NEXT_SPECIAL_FILES = {
|
|
||||||
"layout.ts",
|
|
||||||
"layout.tsx",
|
|
||||||
"page.ts",
|
|
||||||
"page.tsx",
|
|
||||||
"loading.tsx",
|
|
||||||
"error.tsx",
|
|
||||||
"not-found.tsx",
|
|
||||||
"globals.css",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _repo_root(cwd: Path) -> Path:
|
def _repo_root(cwd: Path) -> Path:
|
||||||
@@ -72,13 +62,66 @@ def _normalize(path_text: str) -> str:
|
|||||||
def _is_test_path(path_text: str) -> bool:
|
def _is_test_path(path_text: str) -> bool:
|
||||||
normalized = _normalize(path_text)
|
normalized = _normalize(path_text)
|
||||||
name = normalized.rsplit("/", 1)[-1]
|
name = normalized.rsplit("/", 1)[-1]
|
||||||
|
path = Path(path_text)
|
||||||
return (
|
return (
|
||||||
"__tests__/" in normalized
|
"/tests/" in f"/{normalized}"
|
||||||
|
or "/test/" in f"/{normalized}"
|
||||||
|
or name.endswith("_test.cpp")
|
||||||
|
or name.startswith("test_")
|
||||||
or ".test." in name
|
or ".test." in name
|
||||||
or ".spec." in name
|
or ".spec." in name
|
||||||
or "test" in name
|
) and path.suffix.lower() in TEST_SUFFIXES
|
||||||
or "spec" in name
|
|
||||||
)
|
|
||||||
|
def _token(text: str) -> str:
|
||||||
|
return "".join(ch for ch in text.lower() if ch.isalnum())
|
||||||
|
|
||||||
|
|
||||||
|
def _module_token(path: Path) -> str:
|
||||||
|
parts = [part.lower() for part in path.parts]
|
||||||
|
for marker in ("include", "src"):
|
||||||
|
if marker not in parts:
|
||||||
|
continue
|
||||||
|
idx = parts.index(marker)
|
||||||
|
if marker == "include" and idx + 2 < len(parts) and parts[idx + 1] == "fesa":
|
||||||
|
return _token(parts[idx + 2])
|
||||||
|
if marker == "src" and idx + 1 < len(parts):
|
||||||
|
return _token(parts[idx + 1])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _related_tokens(path: Path) -> set[str]:
|
||||||
|
tokens = {_token(_base_name(path))}
|
||||||
|
module = _module_token(path)
|
||||||
|
if module:
|
||||||
|
tokens.add(module)
|
||||||
|
return {token for token in tokens if token}
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_test_paths(paths: list[str], cwd: Path, root: Path) -> list[Path]:
|
||||||
|
candidates: list[Path] = []
|
||||||
|
for path_text in paths:
|
||||||
|
resolved = _resolve_path(path_text, cwd)
|
||||||
|
if _is_test_path(str(resolved)):
|
||||||
|
candidates.append(resolved)
|
||||||
|
|
||||||
|
for test_root_name in ("tests", "test"):
|
||||||
|
test_root = root / test_root_name
|
||||||
|
if not test_root.is_dir():
|
||||||
|
continue
|
||||||
|
for suffix in TEST_SUFFIXES:
|
||||||
|
candidates.extend(test_root.rglob(f"*{suffix}"))
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _has_related_test(path: Path, candidate_tests: list[Path]) -> bool:
|
||||||
|
tokens = _related_tokens(path)
|
||||||
|
for test_path in candidate_tests:
|
||||||
|
test_token = _token(test_path.stem)
|
||||||
|
if any(token and token in test_token for token in tokens):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _is_exempt(path_text: str) -> bool:
|
def _is_exempt(path_text: str) -> bool:
|
||||||
@@ -86,17 +129,13 @@ def _is_exempt(path_text: str) -> bool:
|
|||||||
path = Path(path_text)
|
path = Path(path_text)
|
||||||
name = path.name.lower()
|
name = path.name.lower()
|
||||||
|
|
||||||
if _is_test_path(path_text):
|
if name == "cmakelists.txt":
|
||||||
return True
|
return True
|
||||||
if name in NEXT_SPECIAL_FILES:
|
if _is_test_path(path_text):
|
||||||
return True
|
return True
|
||||||
if path.suffix.lower() in CONFIG_SUFFIXES:
|
if path.suffix.lower() in CONFIG_SUFFIXES:
|
||||||
return True
|
return True
|
||||||
if ".env" in name or ".config." in name:
|
if "/cmake/" in normalized:
|
||||||
return True
|
|
||||||
if any(token in name for token in ("tailwind", "postcss", "next.config", "tsconfig")):
|
|
||||||
return True
|
|
||||||
if "/types/" in normalized or name in {"types.ts", "types.d.ts"}:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -110,38 +149,15 @@ def _resolve_path(path_text: str, cwd: Path) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def _base_name(path: Path) -> str:
|
def _base_name(path: Path) -> str:
|
||||||
for suffix in (".tsx", ".ts", ".jsx", ".js"):
|
for suffix in sorted(SOURCE_SUFFIXES, key=len, reverse=True):
|
||||||
if path.name.endswith(suffix):
|
if path.name.lower().endswith(suffix):
|
||||||
return path.name[: -len(suffix)]
|
return path.name[: -len(suffix)]
|
||||||
return path.stem
|
return path.stem
|
||||||
|
|
||||||
|
|
||||||
def _has_existing_test(path: Path, root: Path) -> bool:
|
|
||||||
directory = path.parent
|
|
||||||
parent = directory.parent
|
|
||||||
base = _base_name(path)
|
|
||||||
|
|
||||||
for ext in TEST_SUFFIXES:
|
|
||||||
if (directory / f"{base}.test.{ext}").exists():
|
|
||||||
return True
|
|
||||||
if (directory / f"{base}.spec.{ext}").exists():
|
|
||||||
return True
|
|
||||||
|
|
||||||
for ext in TEST_SUFFIXES:
|
|
||||||
if (parent / "__tests__" / f"{base}.test.{ext}").exists():
|
|
||||||
return True
|
|
||||||
if (directory / "__tests__" / f"{base}.test.{ext}").exists():
|
|
||||||
return True
|
|
||||||
|
|
||||||
for ext in TEST_SUFFIXES:
|
|
||||||
if (root / "src" / "__tests__" / f"{base}.test.{ext}").exists():
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]:
|
def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]:
|
||||||
missing_tests: list[str] = []
|
missing_tests: list[str] = []
|
||||||
|
candidate_tests = _candidate_test_paths(paths, cwd, root)
|
||||||
for path_text in paths:
|
for path_text in paths:
|
||||||
if _is_exempt(path_text):
|
if _is_exempt(path_text):
|
||||||
continue
|
continue
|
||||||
@@ -149,7 +165,7 @@ def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]:
|
|||||||
path = _resolve_path(path_text, cwd)
|
path = _resolve_path(path_text, cwd)
|
||||||
if path.suffix.lower() not in SOURCE_SUFFIXES:
|
if path.suffix.lower() not in SOURCE_SUFFIXES:
|
||||||
continue
|
continue
|
||||||
if not _has_existing_test(path, root):
|
if not _has_related_test(path, candidate_tests):
|
||||||
missing_tests.append(_base_name(path))
|
missing_tests.append(_base_name(path))
|
||||||
|
|
||||||
return missing_tests
|
return missing_tests
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
name: harness-review
|
name: harness-review
|
||||||
description: Use when reviewing this Harness repository: local changes, generated phase files, step outputs, implementation diffs, missing tests, build readiness, or compliance with AGENTS.md, docs/ARCHITECTURE.md, docs/ADR.md, and Harness acceptance criteria.
|
description: Use when reviewing this C++/MSVC Harness repository: local changes, generated phase files, step outputs, implementation diffs, missing tests, MSVC build readiness, or compliance with AGENTS.md, docs/ARCHITECTURE.md, docs/ADR.md, and Harness acceptance criteria.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Harness Review
|
# Harness Review
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Use this skill to review Harness work against the repository's persistent rules, architecture docs, and executable verification requirements. Prioritize bugs, regressions, missing tests, and rule violations.
|
Use this skill to review Harness work against the repository's persistent rules, architecture docs, C++/MSVC constraints, TDD guard policy, and executable verification requirements. Prioritize bugs, regressions, missing tests, and rule violations.
|
||||||
|
|
||||||
## Review Process
|
## Review Process
|
||||||
|
|
||||||
1. Read `/AGENTS.md`, `/docs/ARCHITECTURE.md`, and `/docs/ADR.md`.
|
1. Read `/AGENTS.md`, `/docs/ARCHITECTURE.md`, and `/docs/ADR.md`.
|
||||||
2. Inspect the changed files with `git status --short` and `git diff`.
|
2. Inspect the changed files with `git status --short` and `git diff`.
|
||||||
3. Check architecture, stack choices, tests, critical rules, and build readiness.
|
3. Check architecture, stack choices, C++ test coverage, critical rules, and MSVC/CMake readiness.
|
||||||
4. Run relevant verification commands when feasible. If a command cannot be run, report that as residual risk.
|
4. Run relevant verification commands when feasible. If a command cannot be run, report that as residual risk.
|
||||||
5. Lead with actionable findings. Keep summaries secondary.
|
5. Lead with actionable findings. Keep summaries secondary.
|
||||||
|
|
||||||
@@ -21,11 +21,12 @@ Use this skill to review Harness work against the repository's persistent rules,
|
|||||||
|
|
||||||
| Item | Question |
|
| Item | Question |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Architecture | Does the change follow `docs/ARCHITECTURE.md` directory and module boundaries? |
|
| Architecture | Does the change follow `docs/ARCHITECTURE.md` ownership boundaries? |
|
||||||
| Stack | Does the change stay within choices documented in `docs/ADR.md`? |
|
| Stack | Does the change stay within C++/MSVC/CMake decisions documented in `docs/ADR.md`? |
|
||||||
| Tests | Are new or changed behaviors covered by tests? |
|
| Tests | Are new or changed behaviors covered by Python Harness tests or C++ tests? |
|
||||||
|
| TDD Guard | Would C++ production edits be blocked without related tests? |
|
||||||
| Critical Rules | Does the change violate any `AGENTS.md` CRITICAL rule? |
|
| Critical Rules | Does the change violate any `AGENTS.md` CRITICAL rule? |
|
||||||
| Build | Do relevant build/test/lint commands pass? |
|
| Build | Do `python -m unittest discover -s scripts -p "test_*.py"` and `python scripts/validate_workspace.py` pass or provide an expected no-CMake message? |
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
@@ -36,7 +37,8 @@ If there are findings, list them first in severity order with file and line refe
|
|||||||
| 아키텍처 준수 | PASS/FAIL | {상세} |
|
| 아키텍처 준수 | PASS/FAIL | {상세} |
|
||||||
| 기술 스택 준수 | PASS/FAIL | {상세} |
|
| 기술 스택 준수 | PASS/FAIL | {상세} |
|
||||||
| 테스트 존재 | PASS/FAIL | {상세} |
|
| 테스트 존재 | PASS/FAIL | {상세} |
|
||||||
|
| TDD Guard | PASS/FAIL | {상세} |
|
||||||
| CRITICAL 규칙 | PASS/FAIL | {상세} |
|
| CRITICAL 규칙 | PASS/FAIL | {상세} |
|
||||||
| 빌드 가능 | PASS/FAIL | {상세} |
|
| 빌드/검증 가능 | PASS/FAIL | {상세} |
|
||||||
|
|
||||||
When there are no findings, say that clearly, then mention any commands not run or remaining risk.
|
When there are no findings, say that clearly, then mention any commands not run or remaining risk.
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: harness-workflow
|
name: harness-workflow
|
||||||
description: Use when planning or running this Harness framework: reading AGENTS.md and docs/*.md, discussing implementation scope, creating or updating phases/index.json, phases/{task}/index.json, phases/{task}/stepN.md, or invoking scripts/execute.py for staged Codex execution.
|
description: Use when planning or running this C++/MSVC Harness framework: reading AGENTS.md and docs/*.md, discussing implementation scope, creating or updating phases/index.json, phases/{task}/index.json, phases/{task}/stepN.md, or invoking scripts/execute.py for staged Codex execution.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Harness Workflow
|
# Harness Workflow
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Use this skill to turn a user-approved task into small, self-contained Harness steps that another Codex session can execute reliably. Keep the workflow grounded in repository docs and executable acceptance criteria.
|
Use this skill to turn a user-approved task into small, self-contained Harness steps that another Codex session can execute reliably. Keep every step grounded in repository docs, C++/MSVC constraints, TDD, and executable acceptance criteria.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@ Use this skill to turn a user-approved task into small, self-contained Harness s
|
|||||||
- Make every step self-contained. Do not rely on prior conversation; include all required context and file paths.
|
- Make every step self-contained. Do not rely on prior conversation; include all required context and file paths.
|
||||||
- Force context gathering. Each step must tell Codex which docs and previous outputs to read before editing.
|
- Force context gathering. Each step must tell Codex which docs and previous outputs to read before editing.
|
||||||
- Specify interfaces and signatures, not full implementations, unless exact code is required for a constraint.
|
- Specify interfaces and signatures, not full implementations, unless exact code is required for a constraint.
|
||||||
- Put core invariants directly in the step: idempotency, security, data integrity, API contracts, or other non-negotiables.
|
- Put core invariants directly in the step: idempotency, numerical conventions, data integrity, API contracts, or other non-negotiables.
|
||||||
- Use executable acceptance criteria such as `npm run build && npm test`, not abstract statements.
|
- Use executable acceptance criteria such as `python scripts/validate_workspace.py`, not abstract statements.
|
||||||
- Write cautions concretely: "Do not do X. Reason: Y."
|
- For C++ behavior changes, require tests first and name the expected test file or test executable.
|
||||||
- Name steps with kebab-case slugs such as `project-setup`, `api-layer`, or `auth-flow`.
|
- Name steps with kebab-case slugs such as `project-setup`, `core-types`, or `solver-validation`.
|
||||||
|
|
||||||
## Phase Files
|
## Phase Files
|
||||||
|
|
||||||
@@ -47,12 +47,12 @@ Create `phases/{task-name}/index.json`:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"project": "<project-name>",
|
"project": "FESA Harness",
|
||||||
"phase": "<task-name>",
|
"phase": "<task-name>",
|
||||||
"steps": [
|
"steps": [
|
||||||
{ "step": 0, "name": "project-setup", "status": "pending" },
|
{ "step": 0, "name": "project-setup", "status": "pending" },
|
||||||
{ "step": 1, "name": "core-types", "status": "pending" },
|
{ "step": 1, "name": "core-types", "status": "pending" },
|
||||||
{ "step": 2, "name": "api-layer", "status": "pending" }
|
{ "step": 2, "name": "validation-path", "status": "pending" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -85,11 +85,15 @@ Rules:
|
|||||||
|
|
||||||
{Concrete instructions with file paths, interfaces, signatures, and rules.}
|
{Concrete instructions with file paths, interfaces, signatures, and rules.}
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- {Exact C++ or Python test file and behavior to add before implementation.}
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
npm test
|
python scripts/validate_workspace.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 검증 절차
|
## 검증 절차
|
||||||
@@ -99,6 +103,7 @@ npm test
|
|||||||
- ARCHITECTURE.md 디렉토리 구조를 따르는가?
|
- ARCHITECTURE.md 디렉토리 구조를 따르는가?
|
||||||
- ADR 기술 스택을 벗어나지 않았는가?
|
- ADR 기술 스택을 벗어나지 않았는가?
|
||||||
- AGENTS.md CRITICAL 규칙을 위반하지 않았는가?
|
- AGENTS.md CRITICAL 규칙을 위반하지 않았는가?
|
||||||
|
- C++ 변경에는 관련 테스트가 존재하는가?
|
||||||
3. 결과에 따라 `phases/{task-name}/index.json`의 해당 step을 업데이트한다:
|
3. 결과에 따라 `phases/{task-name}/index.json`의 해당 step을 업데이트한다:
|
||||||
- 성공: `"status": "completed"`, `"summary": "산출물 한 줄 요약"`
|
- 성공: `"status": "completed"`, `"summary": "산출물 한 줄 요약"`
|
||||||
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
@@ -106,7 +111,7 @@ npm test
|
|||||||
|
|
||||||
## 금지사항
|
## 금지사항
|
||||||
|
|
||||||
- {Do not do X. Reason: Y.}
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라. Reason: 이 Harness는 C++/MSVC 전용이다.
|
||||||
- 기존 테스트를 깨뜨리지 마라.
|
- 기존 테스트를 깨뜨리지 마라.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+18
-4
@@ -1,8 +1,22 @@
|
|||||||
node_modules/
|
.vs/
|
||||||
.next/
|
build/
|
||||||
out/
|
out/
|
||||||
next-env.d.ts
|
CMakeFiles/
|
||||||
tsconfig.tsbuildinfo
|
CMakeCache.txt
|
||||||
|
cmake_install.cmake
|
||||||
|
CTestTestfile.cmake
|
||||||
|
Testing/
|
||||||
|
*.vcxproj.user
|
||||||
|
*.obj
|
||||||
|
*.pdb
|
||||||
|
*.ilk
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.lib
|
||||||
|
*.exp
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# phase execution outputs
|
# phase execution outputs
|
||||||
phases/**/phase*-output.json
|
phases/**/phase*-output.json
|
||||||
|
|||||||
@@ -1,21 +1,44 @@
|
|||||||
# 프로젝트: {프로젝트명}
|
# Project: FESA Harness
|
||||||
|
|
||||||
## 기술 스택
|
## 기술 스택
|
||||||
- {프레임워크 (예: Next.js 15)}
|
- C++17 이상
|
||||||
- {언어 (예: TypeScript strict mode)}
|
- MSVC on Windows
|
||||||
- {스타일링 (예: Tailwind CSS)}
|
- CMake + CTest
|
||||||
|
- Harness scripts in Python 3
|
||||||
|
|
||||||
## 아키텍처 규칙
|
## 아키텍처 규칙
|
||||||
- CRITICAL: {절대 지켜야 할 규칙 1 (예: 모든 API 로직은 app/api/ 라우트 핸들러에서만 처리)}
|
- CRITICAL: 기본 검증 경로는 `python scripts/validate_workspace.py`이다.
|
||||||
- CRITICAL: {절대 지켜야 할 규칙 2 (예: 클라이언트 컴포넌트에서 직접 외부 API를 호출하지 말 것)}
|
- CRITICAL: C++ 빌드는 CMake/MSVC/x64/Debug 기준으로 검증한다.
|
||||||
- {일반 규칙 (예: 컴포넌트는 components/ 폴더에, 타입은 types/ 폴더에 분리)}
|
- CRITICAL: 새 기능 또는 동작 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현한다.
|
||||||
|
- CRITICAL: Abaqus reference artifact나 solver 코드 복원은 명시적으로 요청된 phase에서만 수행한다.
|
||||||
|
- Harness runner는 `scripts/execute.py`에 둔다.
|
||||||
|
- Codex hook 정책은 `.codex/hooks/`에 둔다.
|
||||||
|
- Harness planning/review instructions are stored in `.codex/skills/`.
|
||||||
|
- Generated phase execution outputs remain ignored under `phases/**/step*-output.json`.
|
||||||
|
|
||||||
## 개발 프로세스
|
## 개발 프로세스
|
||||||
- CRITICAL: 새 기능 구현 시 반드시 테스트를 먼저 작성하고, 테스트가 통과하는 구현을 작성할 것 (TDD)
|
- TDD를 기본으로 한다. C++ production file을 바꿀 때는 관련 C++ test file이 있어야 한다.
|
||||||
- 커밋 메시지는 conventional commits 형식을 따를 것 (feat:, fix:, docs:, refactor:)
|
- 커밋 전 hook은 Harness Python self-test와 workspace validation을 실행해야 한다.
|
||||||
|
- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`.
|
||||||
|
- 계획이 필요한 장기 작업은 Harness phase로 나누고, 각 step은 독립 실행 가능해야 한다.
|
||||||
|
|
||||||
## 명령어
|
## 명령어
|
||||||
npm run dev # 개발 서버
|
```bash
|
||||||
npm run build # 프로덕션 빌드
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
npm run lint # ESLint
|
python scripts/validate_workspace.py
|
||||||
npm run test # 테스트
|
python scripts/execute.py <phase-dir>
|
||||||
|
python scripts/execute.py <phase-dir> --push
|
||||||
|
```
|
||||||
|
|
||||||
|
## MSVC 검증 기본값
|
||||||
|
- Generator: `Visual Studio 17 2022`
|
||||||
|
- Platform: `x64`
|
||||||
|
- Config: `Debug`
|
||||||
|
- Build directory: `build/msvc-debug`
|
||||||
|
|
||||||
|
Override variables:
|
||||||
|
- `HARNESS_VALIDATION_COMMANDS`
|
||||||
|
- `HARNESS_CMAKE_GENERATOR`
|
||||||
|
- `HARNESS_CMAKE_PLATFORM`
|
||||||
|
- `HARNESS_CMAKE_CONFIG`
|
||||||
|
- `HARNESS_BUILD_DIR`
|
||||||
|
|||||||
+26
-13
@@ -1,21 +1,34 @@
|
|||||||
# Architecture Decision Records
|
# Architecture Decision Records
|
||||||
|
|
||||||
## 철학
|
## 철학
|
||||||
{프로젝트의 핵심 가치관 (예: MVP 속도 최우선. 외부 의존성 최소화. 작동하는 최소 구현을 선택.)}
|
Harness는 현재 프로젝트의 실제 기술 스택을 반영해야 한다. C++/MSVC 프로젝트에서 npm, Next.js, TypeScript test naming을 기본값으로 두면 agent prompt와 hook policy가 잘못된 구현을 유도한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ADR-001: {결정 사항 (예: Next.js App Router 선택)}
|
### ADR-001: C++ 전용 Harness
|
||||||
**결정**: {뭘 선택했는지}
|
**결정**: Harness scaffold는 C++/MSVC 전용으로 운영한다. JavaScript/TypeScript fallback은 유지하지 않는다.
|
||||||
**이유**: {왜 선택했는지}
|
|
||||||
**트레이드오프**: {뭘 포기했는지}
|
|
||||||
|
|
||||||
### ADR-002: {결정 사항}
|
**이유**: FESA 개발 환경은 MSVC 기반 C++이다. 언어별 fallback을 남기면 validation, TDD guard, acceptance criteria가 흐려진다.
|
||||||
**결정**: {뭘 선택했는지}
|
|
||||||
**이유**: {왜 선택했는지}
|
|
||||||
**트레이드오프**: {뭘 포기했는지}
|
|
||||||
|
|
||||||
### ADR-003: {결정 사항}
|
**트레이드오프**: 같은 Harness scaffold를 JS/TS 프로젝트에 재사용할 수 없다. 필요하면 별도 template이나 language registry를 새 ADR로 설계한다.
|
||||||
**결정**: {뭘 선택했는지}
|
|
||||||
**이유**: {왜 선택했는지}
|
### ADR-002: CMake/MSVC/x64/Debug 기본 검증
|
||||||
**트레이드오프**: {뭘 포기했는지}
|
**결정**: 기본 workspace validation은 CMake, Visual Studio 17 2022 generator, x64 platform, Debug config, CTest로 수행한다.
|
||||||
|
|
||||||
|
**이유**: MSVC 환경에서 CMake/CTest는 source tree가 복원되거나 새 C++ project가 추가될 때 가장 일관된 build/test entry point다.
|
||||||
|
|
||||||
|
**트레이드오프**: Visual Studio solution-only project는 기본 지원하지 않는다. 명시적으로 필요하면 `HARNESS_VALIDATION_COMMANDS`로 override한다.
|
||||||
|
|
||||||
|
### ADR-003: 엄격한 C++ TDD Guard
|
||||||
|
**결정**: C++ production file 변경은 관련 C++ test file이 없으면 차단한다.
|
||||||
|
|
||||||
|
**이유**: Harness의 핵심 목적은 agent가 검증 없는 C++ 변경을 만들지 않도록 하는 것이다. Header 중심 C++ 구조에서도 module 또는 basename 기반 테스트 존재를 확인한다.
|
||||||
|
|
||||||
|
**트레이드오프**: 초기 scaffolding 작업에서 guard가 엄격하게 느껴질 수 있다. 문서, CMake 설정, Harness metadata는 guard 대상에서 제외한다.
|
||||||
|
|
||||||
|
### ADR-004: Harness 자체 테스트 우선
|
||||||
|
**결정**: commit hook은 먼저 Python Harness self-test를 실행한 뒤 workspace validation을 실행한다.
|
||||||
|
|
||||||
|
**이유**: 현재 저장소에는 C++ source tree가 없을 수 있다. Harness가 스스로 검증 가능해야 이후 phase generation과 source restoration을 안전하게 진행할 수 있다.
|
||||||
|
|
||||||
|
**트레이드오프**: commit 시간이 조금 늘어난다. 대신 hook/validation regressions를 빠르게 잡는다.
|
||||||
|
|||||||
+51
-17
@@ -1,24 +1,58 @@
|
|||||||
# 아키텍처
|
# 아키텍처
|
||||||
|
|
||||||
## 디렉토리 구조
|
## 목표
|
||||||
```
|
이 저장소의 현재 책임은 C++/MSVC 프로젝트를 위한 Codex Harness scaffold를 제공하는 것이다. Harness는 phase execution, edit guard, commit validation, workspace validation을 분리해서 관리한다.
|
||||||
src/
|
|
||||||
├── app/ # 페이지 + API 라우트
|
|
||||||
├── components/ # UI 컴포넌트
|
|
||||||
├── types/ # TypeScript 타입 정의
|
|
||||||
├── lib/ # 유틸리티 + 헬퍼
|
|
||||||
└── services/ # 외부 API 래퍼
|
|
||||||
```
|
|
||||||
|
|
||||||
## 패턴
|
## 디렉토리 구조
|
||||||
{사용하는 디자인 패턴 (예: Server Components 기본, 인터랙션이 필요한 곳만 Client Component)}
|
```text
|
||||||
|
.codex/
|
||||||
|
├── hooks/ # Codex hook scripts
|
||||||
|
└── skills/ # Harness planning/review instructions
|
||||||
|
docs/ # Project and Harness guidance
|
||||||
|
scripts/
|
||||||
|
├── execute.py # Phase step executor
|
||||||
|
├── validate_workspace.py
|
||||||
|
└── test_*.py # Harness self-tests
|
||||||
|
phases/ # Optional generated phase plans
|
||||||
|
```
|
||||||
|
|
||||||
## 데이터 흐름
|
## 데이터 흐름
|
||||||
```
|
```text
|
||||||
{데이터가 어떻게 흐르는지 (예:
|
User-approved task
|
||||||
사용자 입력 → Client Component → API Route → 외부 API → 응답 → UI 업데이트
|
-> Harness phase files under phases/
|
||||||
)}
|
-> scripts/execute.py injects AGENTS.md and docs/*.md
|
||||||
|
-> Codex executes one step at a time
|
||||||
|
-> step updates phases/{phase}/index.json
|
||||||
|
-> validation runs through scripts/validate_workspace.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 상태 관리
|
## Hook 흐름
|
||||||
{상태 관리 방식 (예: 서버 상태는 Server Components, 클라이언트 상태는 useState/useReducer)}
|
```text
|
||||||
|
apply_patch/Edit/Write
|
||||||
|
-> .codex/hooks/tdd-guard.py
|
||||||
|
-> C++ production changes require related tests
|
||||||
|
|
||||||
|
git commit command
|
||||||
|
-> .codex/hooks/pre_commit_checks.py
|
||||||
|
-> Python Harness self-tests
|
||||||
|
-> scripts/validate_workspace.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation 흐름
|
||||||
|
```text
|
||||||
|
HARNESS_VALIDATION_COMMANDS set
|
||||||
|
-> run exact commands
|
||||||
|
|
||||||
|
CMakePresets.json has msvc-debug configure preset
|
||||||
|
-> cmake --preset msvc-debug
|
||||||
|
-> cmake --build preset binary dir --config Debug
|
||||||
|
-> ctest --test-dir preset binary dir -C Debug
|
||||||
|
|
||||||
|
CMakeLists.txt exists
|
||||||
|
-> cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
|
||||||
|
-> cmake --build build/msvc-debug --config Debug
|
||||||
|
-> ctest --test-dir build/msvc-debug --output-on-failure -C Debug
|
||||||
|
|
||||||
|
No CMake project
|
||||||
|
-> print guidance and exit successfully
|
||||||
|
```
|
||||||
|
|||||||
+15
-14
@@ -1,21 +1,22 @@
|
|||||||
# PRD: {프로젝트명}
|
# PRD: C++/MSVC Harness
|
||||||
|
|
||||||
## 목표
|
## 목표
|
||||||
{이 프로젝트가 해결하려는 문제를 한 줄로 요약}
|
Codex Harness가 C++/MSVC 프로젝트에서 phase planning, TDD guard, commit validation, workspace validation을 일관되게 수행하게 한다.
|
||||||
|
|
||||||
## 사용자
|
## 사용자
|
||||||
{누가 이 제품을 쓰는지}
|
- Windows/MSVC 기반 C++ 개발자
|
||||||
|
- Harness phase를 작성하고 실행하는 Codex agent
|
||||||
|
- Harness 결과를 검토하는 reviewer
|
||||||
|
|
||||||
## 핵심 기능
|
## 핵심 기능
|
||||||
1. {기능 1}
|
1. CMake/MSVC/x64/Debug 기반 workspace validation
|
||||||
2. {기능 2}
|
2. C++ source/header 변경에 대한 엄격한 TDD guard
|
||||||
3. {기능 3}
|
3. npm 없이 Python self-test와 CMake/CTest 검증을 수행하는 pre-commit hook
|
||||||
|
4. C++ 프로젝트에 맞는 Harness workflow/review prompt
|
||||||
|
5. CMake project가 아직 없어도 Harness 자체 테스트가 가능한 no-op validation path
|
||||||
|
|
||||||
## MVP 제외 사항
|
## 제외 사항
|
||||||
- {안 만들 것 1}
|
- 이전 FESA solver source tree 복원
|
||||||
- {안 만들 것 2}
|
- JavaScript/TypeScript fallback 유지
|
||||||
- {안 만들 것 3}
|
- Abaqus reference artifact 생성 또는 solver reference 비교 구현
|
||||||
|
- Visual Studio `.sln`/`.vcxproj` 전용 MSBuild workflow
|
||||||
## 디자인
|
|
||||||
- {디자인 방향 (예: 다크모드 고정, 미니멀)}
|
|
||||||
- {색상 (예: 무채색 + 포인트 1가지)}
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def load_pre_commit_checks():
|
||||||
|
module_path = Path(__file__).resolve().parent.parent / ".codex" / "hooks" / "pre_commit_checks.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("pre_commit_checks", module_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class PreCommitChecksTests(unittest.TestCase):
|
||||||
|
def test_git_commit_runs_python_self_tests_and_workspace_validation(self):
|
||||||
|
pre_commit_checks = load_pre_commit_checks()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
commands = pre_commit_checks._build_pre_commit_commands(root)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
commands,
|
||||||
|
[
|
||||||
|
[sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"],
|
||||||
|
[sys.executable, "scripts/validate_workspace.py"],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertFalse(any("npm" in part.lower() for command in commands for part in command))
|
||||||
|
|
||||||
|
def test_only_git_commit_commands_trigger_checks(self):
|
||||||
|
pre_commit_checks = load_pre_commit_checks()
|
||||||
|
self.assertTrue(pre_commit_checks._is_git_commit('git commit -m "change"'))
|
||||||
|
self.assertTrue(pre_commit_checks._is_git_commit('git -c core.editor=true commit -m "change"'))
|
||||||
|
self.assertFalse(pre_commit_checks._is_git_commit("git status --short"))
|
||||||
|
self.assertFalse(pre_commit_checks._is_git_commit("echo git commit"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import importlib.util
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def load_tdd_guard():
|
||||||
|
module_path = Path(__file__).resolve().parent.parent / ".codex" / "hooks" / "tdd-guard.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("tdd_guard", module_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class CppTddGuardTests(unittest.TestCase):
|
||||||
|
def test_cpp_production_file_without_related_test_is_blocked(self):
|
||||||
|
tdd_guard = load_tdd_guard()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
source = root / "include" / "fesa" / "Core" / "DofManager.hpp"
|
||||||
|
source.parent.mkdir(parents=True)
|
||||||
|
source.write_text("#pragma once\n", encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertEqual(tdd_guard._guarded_paths([str(source)], root, root), ["DofManager"])
|
||||||
|
|
||||||
|
def test_cpp_production_file_with_module_test_is_allowed(self):
|
||||||
|
tdd_guard = load_tdd_guard()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
source = root / "include" / "fesa" / "Core" / "DofManager.hpp"
|
||||||
|
source.parent.mkdir(parents=True)
|
||||||
|
source.write_text("#pragma once\n", encoding="utf-8")
|
||||||
|
tests_dir = root / "tests"
|
||||||
|
tests_dir.mkdir()
|
||||||
|
(tests_dir / "test_core_module_includes.cpp").write_text("int main() { return 0; }\n", encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertEqual(tdd_guard._guarded_paths([str(source)], root, root), [])
|
||||||
|
|
||||||
|
def test_cpp_production_file_with_basename_test_in_same_patch_is_allowed(self):
|
||||||
|
tdd_guard = load_tdd_guard()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
source = root / "src" / "Math" / "DenseMatrix.cpp"
|
||||||
|
source.parent.mkdir(parents=True)
|
||||||
|
source.write_text("void f() {}\n", encoding="utf-8")
|
||||||
|
test_path = root / "tests" / "test_dense_matrix.cpp"
|
||||||
|
|
||||||
|
self.assertEqual(tdd_guard._guarded_paths([str(source), str(test_path)], root, root), [])
|
||||||
|
|
||||||
|
def test_cmake_and_docs_are_exempt(self):
|
||||||
|
tdd_guard = load_tdd_guard()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
self.assertEqual(
|
||||||
|
tdd_guard._guarded_paths(["CMakeLists.txt", "docs/ARCHITECTURE.md", "cmake/toolchain.cmake"], root, root),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
def load_validate_workspace():
|
||||||
|
module_path = Path(__file__).resolve().parent / "validate_workspace.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("validate_workspace", module_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateWorkspaceTests(unittest.TestCase):
|
||||||
|
def test_env_commands_override_cmake_detection(self):
|
||||||
|
validate_workspace = load_validate_workspace()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
|
||||||
|
with patch.dict(os.environ, {"HARNESS_VALIDATION_COMMANDS": "echo first\n echo second \n"}, clear=True):
|
||||||
|
self.assertEqual(validate_workspace.discover_commands(root), ["echo first", "echo second"])
|
||||||
|
|
||||||
|
def test_msvc_debug_cmake_commands_are_default_for_cmake_project(self):
|
||||||
|
validate_workspace = load_validate_workspace()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
|
||||||
|
build_dir = root / "build" / "msvc-debug"
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
self.assertEqual(
|
||||||
|
validate_workspace.discover_commands(root),
|
||||||
|
[
|
||||||
|
f'cmake -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64',
|
||||||
|
f'cmake --build "{build_dir}" --config Debug',
|
||||||
|
f'ctest --test-dir "{build_dir}" --output-on-failure -C Debug',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_msvc_debug_configure_preset_is_preferred_when_present(self):
|
||||||
|
validate_workspace = load_validate_workspace()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
|
||||||
|
(root / "CMakePresets.json").write_text(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"configurePresets": [
|
||||||
|
{
|
||||||
|
"name": "msvc-debug",
|
||||||
|
"generator": "Visual Studio 17 2022",
|
||||||
|
"binaryDir": "${sourceDir}/out/msvc-debug"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
self.assertEqual(
|
||||||
|
validate_workspace.discover_commands(root),
|
||||||
|
[
|
||||||
|
"cmake --preset msvc-debug",
|
||||||
|
f'cmake --build "{root / "out" / "msvc-debug"}" --config Debug',
|
||||||
|
f'ctest --test-dir "{root / "out" / "msvc-debug"}" --output-on-failure -C Debug',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_cmake_project_has_no_validation_commands(self):
|
||||||
|
validate_workspace = load_validate_workspace()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
self.assertEqual(validate_workspace.discover_commands(root), [])
|
||||||
|
|
||||||
|
def test_common_cmake_install_path_is_prepended_when_cmake_is_not_on_path(self):
|
||||||
|
validate_workspace = load_validate_workspace()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
common_bin = Path(tmp) / "CMake" / "bin"
|
||||||
|
common_bin.mkdir(parents=True)
|
||||||
|
(common_bin / "cmake.exe").write_text("", encoding="utf-8")
|
||||||
|
(common_bin / "ctest.exe").write_text("", encoding="utf-8")
|
||||||
|
with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin):
|
||||||
|
with patch.object(validate_workspace.shutil, "which", return_value=None):
|
||||||
|
env = validate_workspace.validation_environment({"PATH": "C:\\Windows\\System32"})
|
||||||
|
|
||||||
|
self.assertTrue(env["PATH"].startswith(str(common_bin)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run C++/MSVC Harness validation commands."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_GENERATOR = "Visual Studio 17 2022"
|
||||||
|
DEFAULT_PLATFORM = "x64"
|
||||||
|
DEFAULT_CONFIG = "Debug"
|
||||||
|
DEFAULT_BUILD_DIR = "build/msvc-debug"
|
||||||
|
PRESET_NAME = "msvc-debug"
|
||||||
|
COMMON_CMAKE_BIN = Path(r"C:\Program Files\CMake\bin")
|
||||||
|
|
||||||
|
|
||||||
|
def load_env_commands() -> list[str]:
|
||||||
|
raw = os.environ.get("HARNESS_VALIDATION_COMMANDS", "")
|
||||||
|
return [line.strip() for line in raw.splitlines() if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def _cmake_config() -> tuple[str, str, str, Path]:
|
||||||
|
generator = os.environ.get("HARNESS_CMAKE_GENERATOR", DEFAULT_GENERATOR)
|
||||||
|
platform = os.environ.get("HARNESS_CMAKE_PLATFORM", DEFAULT_PLATFORM)
|
||||||
|
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
|
||||||
|
build_dir = Path(os.environ.get("HARNESS_BUILD_DIR", DEFAULT_BUILD_DIR))
|
||||||
|
return generator, platform, config, build_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _read_presets(root: Path) -> dict:
|
||||||
|
presets_file = root / "CMakePresets.json"
|
||||||
|
if not presets_file.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(presets_file.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _preset_binary_dir(root: Path, preset: dict) -> Path:
|
||||||
|
binary_dir = str(preset.get("binaryDir") or DEFAULT_BUILD_DIR)
|
||||||
|
binary_dir = binary_dir.replace("${sourceDir}", str(root))
|
||||||
|
binary_dir = binary_dir.replace("$sourceDir", str(root))
|
||||||
|
path = Path(binary_dir)
|
||||||
|
return path if path.is_absolute() else root / path
|
||||||
|
|
||||||
|
|
||||||
|
def load_preset_commands(root: Path) -> list[str]:
|
||||||
|
payload = _read_presets(root)
|
||||||
|
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
|
||||||
|
for preset in payload.get("configurePresets", []):
|
||||||
|
if isinstance(preset, dict) and preset.get("name") == PRESET_NAME:
|
||||||
|
build_dir = _preset_binary_dir(root, preset)
|
||||||
|
return [
|
||||||
|
f"cmake --preset {PRESET_NAME}",
|
||||||
|
f'cmake --build "{build_dir}" --config {config}',
|
||||||
|
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def load_cmake_commands(root: Path) -> list[str]:
|
||||||
|
if not (root / "CMakeLists.txt").exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
generator, platform, config, build_dir = _cmake_config()
|
||||||
|
if not build_dir.is_absolute():
|
||||||
|
build_dir = root / build_dir
|
||||||
|
return [
|
||||||
|
f'cmake -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}',
|
||||||
|
f'cmake --build "{build_dir}" --config {config}',
|
||||||
|
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def discover_commands(root: Path) -> list[str]:
|
||||||
|
env_commands = load_env_commands()
|
||||||
|
if env_commands:
|
||||||
|
return env_commands
|
||||||
|
preset_commands = load_preset_commands(root)
|
||||||
|
if preset_commands:
|
||||||
|
return preset_commands
|
||||||
|
return load_cmake_commands(root)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(command: str, root: Path) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(
|
||||||
|
command,
|
||||||
|
cwd=root,
|
||||||
|
shell=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
env=validation_environment(os.environ),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validation_environment(base_env: os._Environ | dict[str, str]) -> dict[str, str]:
|
||||||
|
env = dict(base_env)
|
||||||
|
if shutil.which("cmake") is not None:
|
||||||
|
return env
|
||||||
|
cmake_exe = COMMON_CMAKE_BIN / "cmake.exe"
|
||||||
|
if not cmake_exe.exists():
|
||||||
|
return env
|
||||||
|
|
||||||
|
current_path = env.get("PATH", "")
|
||||||
|
paths = [part for part in current_path.split(os.pathsep) if part]
|
||||||
|
common_bin_text = str(COMMON_CMAKE_BIN)
|
||||||
|
if not any(part.lower() == common_bin_text.lower() for part in paths):
|
||||||
|
env["PATH"] = common_bin_text + (os.pathsep + current_path if current_path else "")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def emit_stream(prefix: str, content: str, *, stream) -> None:
|
||||||
|
text = (content or "").strip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
print(prefix, file=stream)
|
||||||
|
print(text, file=stream)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
root = Path(__file__).resolve().parent.parent
|
||||||
|
commands = discover_commands(root)
|
||||||
|
|
||||||
|
if not commands:
|
||||||
|
print("No C++ validation commands configured.")
|
||||||
|
print("Add CMakeLists.txt or set HARNESS_VALIDATION_COMMANDS.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for command in commands:
|
||||||
|
print(f"$ {command}")
|
||||||
|
result = run_command(command, root)
|
||||||
|
emit_stream("[stdout]", result.stdout, stream=sys.stdout)
|
||||||
|
emit_stream("[stderr]", result.stderr, stream=sys.stderr)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Validation failed: {command}", file=sys.stderr)
|
||||||
|
return result.returncode
|
||||||
|
|
||||||
|
print("Validation succeeded.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user