Compare commits

...

16 Commits

Author SHA1 Message Date
KOKO\Mimi 8faaf4db99 docs(build): document dependency package setup 2026-07-30 13:22:03 +09:00
KOKO\Mimi 202e66c862 build(cmake): isolate test-only dependencies 2026-07-30 13:22:03 +09:00
KOKO\Mimi 85b580bb23 fix(harness): retry timed-out Codex steps 2026-07-30 13:22:03 +09:00
KOKO\Mimi 5b6570a1b0 chore(solver-bootstrap): mark phase completed 2026-07-30 13:22:03 +09:00
KOKO\Mimi a407891827 chore(solver-bootstrap): step 3 output 2026-07-30 13:22:03 +09:00
KOKO\Mimi 34d21f43fe feat(solver-bootstrap): step 3 — core-ids-and-diagnostics 2026-07-30 13:22:03 +09:00
KOKO\Mimi 3e44deed72 chore(solver-bootstrap): step 2 output 2026-07-30 13:22:02 +09:00
KOKO\Mimi e0348d80bf feat(solver-bootstrap): step 2 — dependency-smoke-tests 2026-07-30 13:22:02 +09:00
KOKO\Mimi 644d307abe docs(deps): record local dependency packages 2026-07-30 13:22:02 +09:00
KOKO\Mimi 9a32e9b6f7 chore(solver-bootstrap): step 1 output 2026-07-30 13:22:02 +09:00
KOKO\Mimi fc5493bd59 feat(solver-bootstrap): step 1 — cmake-project-scaffold 2026-07-30 13:22:02 +09:00
KOKO\Mimi 4157ba2ff7 docs(toolchain): adopt Visual Studio 2026 v145 2026-07-30 13:22:02 +09:00
KOKO\Mimi 54bfdc4613 chore(solver-bootstrap): step 0 output 2026-07-30 13:22:02 +09:00
KOKO\Mimi 051434b388 feat(solver-bootstrap): step 0 — harness-self-tests 2026-07-30 13:22:02 +09:00
KOKO\Mimi 026c9eea7c fix(harness): allow explicit Codex tool directories 2026-07-30 13:22:02 +09:00
KOKO\Mimi b0ab8e77d5 docs(phases): add harness self-test bootstrap step 2026-07-30 13:22:02 +09:00
40 changed files with 1483 additions and 150 deletions
+1
View File
@@ -2,4 +2,5 @@ __pycache__/
.pytest_cache/
*.py[cod]
.harness/build/
out/
.worktrees/
+11
View File
@@ -0,0 +1,11 @@
{
"version": 1,
"projectType": "cmake",
"cmake": {
"sourceDir": ".",
"binaryDir": "out/build/windows-debug",
"configurePreset": "windows-debug",
"buildPreset": "windows-debug",
"testPreset": "windows-debug"
}
}
+1 -1
View File
@@ -13,7 +13,7 @@
## 기술 기준
- 언어 표준: C++20
- 컴파일러: Visual Studio 2022 MSVC v143
- 컴파일러: Visual Studio 2026 MSVC v145
- 대상 플랫폼: Windows x64
- 빌드 및 테스트: CMake, CMake Presets, CTest, GoogleTest/GoogleMock
- 수치 연산 및 희소 직접해법: Intel oneAPI MKL
+50
View File
@@ -0,0 +1,50 @@
cmake_minimum_required(VERSION 3.30)
project(FESA VERSION 0.1.0 LANGUAGES CXX)
if(NOT MSVC)
message(FATAL_ERROR "FESA requires the Microsoft Visual C++ compiler (MSVC v145).")
endif()
if(NOT CMAKE_VS_PLATFORM_TOOLSET STREQUAL "v145")
message(
FATAL_ERROR
"FESA requires the v145 platform toolset; configured toolset is "
"'${CMAKE_VS_PLATFORM_TOOLSET}'."
)
endif()
if(NOT CMAKE_GENERATOR_PLATFORM STREQUAL "x64")
message(FATAL_ERROR "FESA requires the x64 generator platform.")
endif()
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "FESA requires a 64-bit target.")
endif()
include(CTest)
include(cmake/FesaDependencies.cmake)
add_library(fesa_core STATIC
src/fesa/core/version.cpp
)
target_include_directories(fesa_core
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
target_compile_features(fesa_core PUBLIC cxx_std_20)
target_compile_options(fesa_core PRIVATE /W4 /permissive- /EHsc)
add_executable(fesa
src/fesa/cli/main.cpp
)
target_link_libraries(fesa PRIVATE fesa_core)
target_compile_features(fesa PRIVATE cxx_std_20)
target_compile_options(fesa PRIVATE /W4 /permissive- /EHsc)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
+79
View File
@@ -0,0 +1,79 @@
{
"version": 6,
"configurePresets": [
{
"name": "windows-debug",
"displayName": "Windows Debug",
"generator": "Visual Studio 18 2026",
"architecture": {
"value": "x64",
"strategy": "set"
},
"toolset": {
"value": "v145",
"strategy": "set"
},
"binaryDir": "${sourceDir}/out/build/windows-debug",
"cacheVariables": {
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_STANDARD": "20",
"CMAKE_CXX_STANDARD_REQUIRED": "ON"
}
},
{
"name": "windows-release",
"displayName": "Windows Release",
"generator": "Visual Studio 18 2026",
"architecture": {
"value": "x64",
"strategy": "set"
},
"toolset": {
"value": "v145",
"strategy": "set"
},
"binaryDir": "${sourceDir}/out/build/windows-release",
"cacheVariables": {
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_STANDARD": "20",
"CMAKE_CXX_STANDARD_REQUIRED": "ON"
}
}
],
"buildPresets": [
{
"name": "windows-debug",
"configurePreset": "windows-debug",
"configuration": "Debug"
},
{
"name": "windows-release",
"configurePreset": "windows-release",
"configuration": "Release"
}
],
"testPresets": [
{
"name": "windows-debug",
"configurePreset": "windows-debug",
"configuration": "Debug",
"output": {
"outputOnFailure": true
},
"execution": {
"noTestsAction": "error"
}
},
{
"name": "windows-release",
"configurePreset": "windows-release",
"configuration": "Release",
"output": {
"outputOnFailure": true
},
"execution": {
"noTestsAction": "error"
}
}
]
}
+60
View File
@@ -0,0 +1,60 @@
include_guard(GLOBAL)
if(NOT WIN32 OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "FESA dependencies require a Windows x64 build.")
endif()
set(MKL_ARCH intel64)
set(MKL_INTERFACE lp64)
set(MKL_LINK dynamic)
set(MKL_THREADING tbb_thread)
find_package(MKL CONFIG REQUIRED)
find_package(TBB CONFIG REQUIRED COMPONENTS tbb)
find_package(HDF5 CONFIG REQUIRED COMPONENTS C shared)
if(BUILD_TESTING)
find_package(GTest CONFIG REQUIRED)
endif()
function(fesa_require_imported_target target_name)
if(NOT TARGET "${target_name}")
message(
FATAL_ERROR
"Required imported target '${target_name}' is unavailable. "
"Verify the installed x64 package and its runtime DLLs."
)
endif()
get_target_property(target_is_imported "${target_name}" IMPORTED)
if(NOT target_is_imported)
message(
FATAL_ERROR
"Dependency target '${target_name}' must be provided by an "
"installed package."
)
endif()
endfunction()
fesa_require_imported_target(MKL::MKL)
fesa_require_imported_target(TBB::tbb)
fesa_require_imported_target(hdf5::hdf5-shared)
if(BUILD_TESTING)
fesa_require_imported_target(GTest::gtest_main)
fesa_require_imported_target(GTest::gtest)
fesa_require_imported_target(GTest::gmock)
endif()
add_library(HDF5::HDF5 INTERFACE IMPORTED GLOBAL)
set_property(
TARGET HDF5::HDF5
PROPERTY INTERFACE_LINK_LIBRARIES hdf5::hdf5-shared
)
set(
FESA_DEPENDENCY_RUNTIME_MODIFICATIONS
"PATH=path_list_prepend:${MKL_DLL_DIR}"
"PATH=path_list_prepend:$<TARGET_FILE_DIR:TBB::tbb>"
"PATH=path_list_prepend:$<TARGET_FILE_DIR:hdf5::hdf5-shared>"
)
+23 -1
View File
@@ -6,7 +6,7 @@
## ADR-001: C++20, MSVC v143, x64와 CMake Presets
**상태:** Accepted
**상태:** Superseded by ADR-016
**상황:** 첫 배포는 Windows 개발팀 내부 검증용이며 Intel oneAPI와 HDF5를 일관되게
연동하고 Harness에서 자동 검증해야 한다.
@@ -275,3 +275,25 @@ flat `Domain`으로 정규화한다. 외부 entity는 `(instance name, part-loca
- 요청한 파일이 없으면 실패하며 비요청 물리량을 통과로 오인하지 않는다.
- 단일 Instance에서는 Instance 열을 생략할 수 있다.
- tolerance와 검증 출처는 test registration과 `docs/VALIDATION.md`에서 관리한다.
## ADR-016: Visual Studio 2026 MSVC v145로 툴체인 갱신
**상태:** Accepted
**상황:** 개발 환경에 Visual Studio 2026 Community와 MSVC v145가 설치되어 있고,
CMake 4.4.0이 `Visual Studio 18 2026` 생성기를 지원한다. 반면 기존 ADR-001의
Visual Studio 2022 MSVC v143은 설치되어 있지 않아 solver bootstrap을 진행할 수
없다.
**결정:** ADR-001의 컴파일러 선택을 대체해 C++20, Visual Studio 2026 MSVC v145,
Windows x64를 사용한다. CMake Preset은 `Visual Studio 18 2026` 생성기와 `v145`
toolset을 명시한다. CMake, CMake Presets, CTest 및 GoogleTest/GoogleMock 선택은
유지한다.
**결과와 트레이드오프:**
- 현재 설치된 개발 환경에서 별도 v143 설치 없이 bootstrap을 진행할 수 있다.
- 빌드 계약이 Visual Studio 2026과 v145에 고정되므로 이전 MSVC toolset은 Phase 1
보장 대상이 아니다.
- 컴파일러 갱신에 따른 경고와 표준 라이브러리 동작은 전체 Debug/Release 검증에서
다시 확인해야 한다.
+36 -1
View File
@@ -20,6 +20,20 @@ python scripts/execute.py <phase-name>
python scripts/execute.py <phase-name> --push
```
Codex의 `workspace-write` sandbox 밖에 설치된 실행 도구나 runtime이 필요한 경우에만
`--codex-add-dir`를 반복 지정한다. 경로는 존재하는 절대 디렉터리여야 하며, Codex
CLI의 추가 writable directory와 child PATH 끝에 함께 전달된다.
```powershell
python scripts/execute.py <phase-name> `
--codex-add-dir "C:\path\to\tool-bin" `
--codex-add-dir "C:\path\to\runtime"
```
이 옵션은 지정한 디렉터리에 child agent의 쓰기 권한도 부여하므로, 사용자 프로필이나
드라이브 루트처럼 넓은 경로를 지정하지 말고 실행에 필요한 최소 설치 디렉터리만
허용한다.
## Harness Python 검증
이 저장소의 테스트와 최종 acceptance 검증은 pytest를 시스템 Python에 설치하지 않고
@@ -35,6 +49,27 @@ uv run --with pytest python -m pytest -v -rs
`binaryDir`, `configurePreset`, `buildPreset`, `testPreset`은 preset을 사용할 때 함께
지정해야 한다. 빌드 산출물은 저장소의 `.harness/build/`처럼 격리된 경로에 둔다.
FESA의 외부 패키지 위치는 개발 머신마다 다르므로 tracked preset이나 production
CMake 기본값에 절대경로를 넣지 않는다. 새 PowerShell 세션에서는 configure 전에
다음 package directory 환경 변수를 설정한다. 아래 값은 현재 검증된 개발 환경이며,
설치 버전이나 위치가 다르면 해당 `*Config.cmake`가 있는 디렉터리로 바꾼다.
```powershell
$env:MKL_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\mkl"
$env:TBB_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\tbb"
$env:HDF5_DIR = "C:\Program Files\HDF_Group\HDF5\2.1.1\cmake"
$env:GTest_DIR = "C:\Users\baram\AppData\Local\FESA\dependencies\googletest-1.17.0-v145-x64-crt\lib\cmake\GTest"
Test-Path "$env:MKL_DIR\MKLConfig.cmake"
Test-Path "$env:TBB_DIR\TBBConfig.cmake"
Test-Path "$env:HDF5_DIR\hdf5-config.cmake"
Test-Path "$env:GTest_DIR\GTestConfig.cmake"
```
네 확인 명령이 모두 `True`인 같은 세션에서 preset을 실행한다. oneAPI는 component별
별칭 디렉터리가 아니라 위 통합 `2026.1` package를 사용해야 HDF5의 Intel runtime
의존성까지 CTest PATH에 포함된다.
```json
{
"version": 1,
@@ -50,7 +85,7 @@ uv run --with pytest python -m pytest -v -rs
```
```powershell
cmake --preset windows-debug
cmake --fresh --preset windows-debug
cmake --build --preset windows-debug
ctest --preset windows-debug --output-on-failure
```
@@ -6,7 +6,7 @@
**Architecture:** Implement one end-to-end vertical slice first, then complete the input, numerical, parallel, result, and reference-verification contracts behind the module boundaries in `docs/ARCHITECTURE.md`. Keep the semantic model independent of Abaqus syntax and isolate oneMKL, oneTBB, and HDF5 behind adapters.
**Tech Stack:** C++20, Visual Studio 2022 MSVC v143 x64, CMake/CMake Presets/CTest, GoogleTest/GoogleMock, Intel oneAPI MKL PARDISO, Intel oneAPI TBB, HDF5 C API, Python 3 Harness.
**Tech Stack:** C++20, Visual Studio 2026 MSVC v145 x64, CMake/CMake Presets/CTest, GoogleTest/GoogleMock, Intel oneAPI MKL PARDISO, Intel oneAPI TBB, HDF5 C API, Python 3 Harness.
## Global Constraints
@@ -28,12 +28,18 @@
The planning environment currently has:
- CMake 4.4.0
- Visual Studio Community 2026 at `C:/Program Files/Microsoft Visual Studio/18/Community`
- MSVC v145 tools at `C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.51.36231`
- CMake generator `Visual Studio 18 2026`
- oneMKL CMake package at `C:/Program Files (x86)/Intel/oneAPI/2026.1/lib/cmake/mkl`
- oneTBB CMake package at `C:/Program Files (x86)/Intel/oneAPI/2026.1/lib/cmake/tbb`
- no HDF5 or GoogleTest CMake package found in the standard Program Files trees
- `MSBuild.exe` not currently available on `PATH`
- HDF5 2.1.1 CMake package at `C:/Program Files/HDF_Group/HDF5/2.1.1/cmake`
- GoogleTest/GoogleMock v1.17 VS2026/v145 x64 package at
`C:/Users/baram/AppData/Local/FESA/dependencies/googletest-1.17.0-v145-x64-crt`
- `MSBuild.exe` at `C:/Program Files/Microsoft Visual Studio/18/Community/MSBuild/Current/Bin/MSBuild.exe`
Task 1 must stop as `blocked` rather than downloading packages if HDF5, GoogleTest, or the MSVC toolchain is still unavailable.
The solver bootstrap must stop as `blocked` rather than downloading packages if a dependency
required by its current step is unavailable.
## Required Research Record
@@ -73,7 +79,7 @@ intentionally differs.
| Order | Harness phase | Plan tasks | Independent deliverable |
| ---: | --- | --- | --- |
| 0 | `solver-bootstrap` | 1-2 | Reproducible C++20 build, dependency smoke tests, core IDs and diagnostics |
| 0 | `solver-bootstrap` | Harness baseline, 1-2 | Harness self-tests, reproducible C++20 build, dependency smoke tests, core IDs and diagnostics |
| 1 | `domain-and-input-skeleton` | 3-4 | Flat or single-Instance B31 input becomes an immutable normalized `Domain` |
| 2 | `fem-and-beam-kernel` | 5-6 | Real Timoshenko Beam local stiffness with analytical sanity tests |
| 3 | `equation-and-linear-solve` | 7-8 | Deterministic serial CSR system solved by PARDISO |
@@ -123,6 +129,9 @@ docs/VALIDATION.md benchmark matrix and qualification result
**Files:**
- Create before CMake bootstrap: `tests/harness/test_config.py`
- Create before CMake bootstrap: `tests/harness/test_discovery.py`
- Create before CMake bootstrap: `tests/harness/test_process.py`
- Create: `CMakeLists.txt`
- Create: `CMakePresets.json`
- Create: `cmake/FesaDependencies.cmake`
@@ -141,20 +150,31 @@ docs/VALIDATION.md benchmark matrix and qualification result
- Produces presets: `windows-debug`, `windows-release`
- Later tasks consume the common warning and include-directory policies.
Before the CMake task, add characterization tests for the existing Harness
configuration, project discovery, and validation-result contracts. This
precondition makes the repository-level pytest command collect at least one
test without changing production Harness behavior.
- [ ] **Step 1: Verify required installed packages without changing the machine**
Run:
```powershell
cmake --version
Get-Command MSBuild.exe
& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" `
-latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
-property installationPath
Test-Path "C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe"
Get-ChildItem "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC" -Directory
Get-ChildItem "C:\Program Files (x86)\Intel\oneAPI" -Recurse -Filter MKLConfig.cmake
Get-ChildItem "C:\Program Files (x86)\Intel\oneAPI" -Recurse -Filter TBBConfig.cmake
Get-ChildItem "C:\Program Files" -Recurse -Filter hdf5-config.cmake
Get-ChildItem "C:\Program Files" -Recurse -Filter GTestConfig.cmake
```
Expected: MSVC, oneMKL, oneTBB, HDF5, and GoogleTest are all discoverable. If any are missing, mark the Harness step `blocked` and name the missing package; do not download it.
Expected: Visual Studio 2026/MSVC v145, oneMKL, oneTBB, HDF5, and GoogleTest
are all discoverable before the step that consumes each dependency. If a required item is
missing, mark that Harness step `blocked` and name it; do not download it.
- [ ] **Step 2: Write the failing version test**
@@ -231,9 +251,11 @@ git commit -m "build: bootstrap FESA CMake project"
- Create: `include/fesa/core/vec3.hpp`
- Create: `include/fesa/core/source_location.hpp`
- Create: `include/fesa/core/diagnostic.hpp`
- Create: `include/fesa/core/status.hpp`
- Create: `tests/unit/core/entity_id_test.cpp`
- Create: `tests/unit/core/vec3_test.cpp`
- Create: `tests/unit/core/diagnostic_test.cpp`
- Create: `tests/unit/core/status_test.cpp`
- Modify: `CMakeLists.txt`
- Modify: `tests/CMakeLists.txt`
@@ -263,21 +285,28 @@ struct SourceLocation final {
};
enum class DiagnosticStage {
io, lexical, syntax, semantic, model, equation, solver, results
io, syntax, semantic, model, equation, solver, results, validation
};
enum class Severity { warning, error };
struct Diagnostic final {
DiagnosticStage stage;
Severity severity;
std::string code;
std::string message;
std::optional<SourceLocation> source;
std::optional<std::int64_t> entity_id;
};
struct Status final {
bool succeeded;
std::vector<Diagnostic> diagnostics;
};
```
- [ ] **Step 1: Write failing tests**
Cover negative/zero entity-ID rejection, typed-ID non-interchangeability at compile time, finite `Vec3` validation, and full diagnostic context preservation.
Cover negative entity-ID rejection, typed-ID non-interchangeability at compile time, finite `Vec3` validation, full diagnostic context preservation, and success/failure status preservation.
- [ ] **Step 2: Run focused tests and confirm compile or assertion failure**
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <optional>
#include <string>
#include <fesa/core/source_location.hpp>
namespace fesa {
enum class DiagnosticStage {
io,
syntax,
semantic,
model,
equation,
solver,
results,
validation
};
enum class Severity { warning, error };
struct Diagnostic final {
DiagnosticStage stage;
Severity severity;
std::string code;
std::string message;
std::optional<SourceLocation> source;
};
} // namespace fesa
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <compare>
#include <cstdint>
#include <stdexcept>
namespace fesa {
template <class Tag>
class EntityId final {
public:
explicit constexpr EntityId(const std::int64_t value) : value_{value} {
if (value < 0) {
throw std::invalid_argument{"EntityId value must not be negative."};
}
}
[[nodiscard]] constexpr std::int64_t value() const noexcept {
return value_;
}
auto operator<=>(const EntityId&) const = default;
private:
std::int64_t value_;
};
} // namespace fesa
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <cstddef>
#include <filesystem>
namespace fesa {
struct SourceLocation final {
std::filesystem::path file;
std::size_t line;
std::size_t column;
};
} // namespace fesa
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <vector>
#include <fesa/core/diagnostic.hpp>
namespace fesa {
struct Status final {
bool succeeded;
std::vector<Diagnostic> diagnostics;
};
} // namespace fesa
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <cmath>
namespace fesa {
struct Vec3 final {
double x;
double y;
double z;
};
[[nodiscard]] inline bool is_finite(const Vec3 value) noexcept {
return std::isfinite(value.x) && std::isfinite(value.y) &&
std::isfinite(value.z);
}
} // namespace fesa
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <string_view>
namespace fesa {
[[nodiscard]] std::string_view version() noexcept;
}
+2 -1
View File
@@ -2,7 +2,8 @@
"phases": [
{
"dir": "solver-bootstrap",
"status": "pending"
"status": "completed",
"completed_at": "2026-07-30T13:01:15+0900"
},
{
"dir": "domain-and-input-skeleton",
+1 -1
View File
@@ -18,7 +18,7 @@ packaging 전에 `docs/BUILDING.md`, `docs/INPUT_FORMAT.md`,
체크리스트는 다음 증거 위치와 command를 포함해야 한다.
- MSVC v143 x64, C++20 및 dependency versions
- Visual Studio 2026 MSVC v145 x64, C++20 및 dependency versions
- Debug/Release configure, build, zero-warning, nonzero CTest count
- flat 및 단일 무변환 Instance example
- HDF5 schema inspection
+25 -6
View File
@@ -4,18 +4,37 @@
"steps": [
{
"step": 0,
"name": "cmake-project-scaffold",
"status": "pending"
"name": "harness-self-tests",
"status": "completed",
"summary": "Added tests/harness/test_config.py, test_discovery.py, and test_process.py characterization tests for documented Harness contracts.",
"started_at": "2026-07-29T23:58:15+0900",
"completed_at": "2026-07-30T00:02:27+0900"
},
{
"step": 1,
"name": "dependency-smoke-tests",
"status": "pending"
"name": "cmake-project-scaffold",
"status": "completed",
"summary": "Added the CMake/CTest presets and Harness config plus the fesa_core version API and fesa --version CLI scaffold.",
"started_at": "2026-07-30T00:24:06+0900",
"completed_at": "2026-07-30T00:46:21+0900"
},
{
"step": 2,
"name": "dependency-smoke-tests",
"status": "completed",
"started_at": "2026-07-30T12:18:16+0900",
"summary": "Added installed-package discovery for MKL::MKL (LP64/dynamic/TBB), TBB::tbb, hdf5::hdf5-shared via HDF5::HDF5, and GoogleTest/GoogleMock plus a runtime dependency smoke test.",
"completed_at": "2026-07-30T12:51:15+0900"
},
{
"step": 3,
"name": "core-ids-and-diagnostics",
"status": "pending"
"status": "completed",
"summary": "Added dependency-free EntityId, Vec3 finiteness validation, SourceLocation/Diagnostic/Status headers, and core unit tests.",
"started_at": "2026-07-30T12:53:17+0900",
"completed_at": "2026-07-30T13:01:14+0900"
}
]
],
"created_at": "2026-07-29T23:58:15+0900",
"completed_at": "2026-07-30T13:01:15+0900"
}
File diff suppressed because one or more lines are too long
+40 -37
View File
@@ -1,62 +1,65 @@
# Step 0: CMake Project Scaffold
# Step 0: Harness Self Tests
## 읽어야 할 파일
먼저 아래 파일을 모두 읽고 저장소 계약을 파악하라.
먼저 아래 파일을 모두 읽고 현재 Harness 계약을 파악하라.
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/HARNESS.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/.harness/config.example.json`
- `/pytest.ini`
- `/scripts/msvc_harness/config.py`
- `/scripts/msvc_harness/discovery.py`
- `/scripts/msvc_harness/models.py`
- `/scripts/msvc_harness/process.py`
- `/scripts/msvc_harness/tdd_policy.py`
- `/scripts/hooks/stop_validation.py`
## 작업
C++20/MSVC x64 프로젝트의 최소 실행 가능한 build/test 뼈대를 만든다.
현재 Harness Python 검증이 테스트 0개 수집으로 실패하는 기준선 문제를 해결한다.
production Harness 동작은 변경하지 않고, 이미 문서화된 핵심 계약을 검증하는 최소
자가 테스트를 작성한다.
- `CMakeLists.txt`, `CMakePresets.json`, `cmake/FesaDependencies.cmake`,
`.harness/config.json`, `tests/CMakeLists.txt`를 만든다.
- 실제 target은 `fesa_core` 정적 라이브러리와 `fesa` CLI 두 개만 만든다.
- `include/fesa/core/version.hpp`, `src/fesa/core/version.cpp`,
`src/fesa/cli/main.cpp`에 다음 계약을 구현한다.
- 다음 파일을 만든다.
- `tests/harness/test_config.py`
- `tests/harness/test_discovery.py`
- `tests/harness/test_process.py`
- `test_config.py`는 기본 설정 로드, repository 밖 경로 거부, CMake preset 필드의
all-or-none 규칙을 실제 임시 디렉터리로 검증한다.
- `test_discovery.py`는 빈 저장소 건너뛰기, CMake 우선 감지, C/C++ 파일만 존재하는
orphan 프로젝트 거부를 실제 임시 디렉터리로 검증한다.
- `test_process.py`는 CTest JSON에 테스트가 0개이면 실패하고 하나 이상이면 통과하는
결과 검사를 안전한 fake command runner로 검증한다. subprocess shell을 사용하지
않는 계약도 유지한다.
- 각 테스트는 공개 함수인 `load_config`, `discover_project`, `execute_plan`을 통해
관찰 가능한 동작만 확인한다.
```cpp
namespace fesa {
[[nodiscard]] std::string_view version() noexcept;
}
```
- CLI는 이 step에서 `fesa --version`만 처리한다.
- 먼저 `VersionCommand` CTest를 등록해 실패를 확인한 뒤 최소 구현한다.
- `windows-debug`, `windows-release` configure/build/test preset을 정의한다.
- build 산출물은 `out/build/<preset>` 아래에만 둔다.
- MSVC가 아니거나 x64가 아니면 configure 단계에서 명확히 실패시킨다.
이 step은 기존 production 동작의 characterization test만 추가한다. 테스트가 기존
문서 계약과 다르게 실패하면 production 코드를 임의로 고치지 말고 step을 `blocked`
표시하고 불일치를 기록한다.
## Acceptance Criteria
```powershell
uv run --with pytest python -m pytest -v -rs
cmake --preset windows-debug
cmake --build --preset windows-debug
ctest --preset windows-debug -R VersionCommand --output-on-failure
ctest --preset windows-debug --output-on-failure
```
`fesa --version`은 비어 있지 않은 FESA 버전을 출력하고 종료 코드 0을 반환해야 한다.
CTest는 0개가 아니어야 한다.
pytest가 테스트를 한 개 이상 수집하고 모든 테스트가 통과해야 한다.
## 검증 절차
1. 테스트가 구현 전 실패하는 것을 확인한다.
2. Acceptance Criteria 명령을 새로 실행한다.
3. MSVC/C++20/x64와 산출물 경로를 확인한다.
4. 성공 시 index의 step을 `completed`로 바꾸고 생성 파일을 summary에 기록한다.
5. 도구가 없으면 자동 설치하지 말고 `blocked`와 정확한 누락 항목을 기록한다.
1. 변경 전 명령이 테스트 0개 수집으로 실패한다는 기존 기준선 기록을 확인한다.
2. 위 세 테스트 파일만 추가한다.
3. Acceptance Criteria 명령을 새로 실행한다.
4. production Harness 파일에 변경이 없는지 `git diff`로 확인한다.
5. 성공 시 index의 step을 `completed`로 바꾸고 테스트 파일을 summary에 기록한다.
## 금지사항
- 외부 패키지를 다운로드하지 마라. 이유: 사전 설치 의존성 정책을 위반한다.
- MKL, TBB, HDF5 기능을 구현하지 마라. 이유: 다음 step의 독립 범위다.
- 빈 미래 모듈을 만들지 마라. 이유: Phase 1 최소 실체화 원칙을 위반한다.
- production Harness 코드를 변경하지 마라. 이유: 이 step은 기존 계약의 기준선
테스트만 마련한다.
- C++ 프로젝트 파일이나 `.harness/config.json`을 만들지 마라. 이유: 다음 step의
독립 범위다.
- subprocess로 실제 CMake, MSBuild 또는 CTest를 실행하지 마라. 이유: self-test가
개발 머신 도구 설치 상태에 의존하게 된다.
File diff suppressed because one or more lines are too long
+40 -27
View File
@@ -1,55 +1,68 @@
# Step 1: Dependency Smoke Tests
# Step 1: CMake Project Scaffold
## 읽어야 할 파일
먼저 아래 파일을 모두 읽고 저장소 계약을 파악하라.
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/HARNESS.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/CMakeLists.txt`
- `/CMakePresets.json`
- `/cmake/FesaDependencies.cmake`
- `/.harness/config.json`
- `/tests/CMakeLists.txt`
이전 step의 CMake target과 preset을 그대로 확장하라.
- `/.harness/config.example.json`
- `/tests/harness/test_config.py`
- `/tests/harness/test_discovery.py`
- `/tests/harness/test_process.py`
## 작업
사전 설치된 oneMKL, oneTBB, HDF5 C API, GoogleTest/GoogleMock을 CMake imported
target으로 찾고 링크 계약을 검증한다.
C++20/Visual Studio 2026 MSVC v145 x64 프로젝트의 최소 실행 가능한 build/test
뼈대를 만든다.
- `tests/unit/dependencies/dependency_smoke_test.cpp`를 먼저 작성한다.
- 테스트는 MKL의 작은 vector 연산, TBB의 제한된 parallel loop, HDF5 임시 파일
생성·닫기, GoogleTest 실행을 확인한다.
- `FesaDependencies.cmake` `MKL::MKL`, TBB imported target, HDF5 C target,
GoogleTest target을 제공해야 한다.
- oneMKL은 LP64, dynamic link, TBB threading 조합을 사용한다.
- runtime DLL 또는 architecture 불일치는 configure diagnostic으로 보고한다.
- `CMakeLists.txt`, `CMakePresets.json`, `cmake/FesaDependencies.cmake`,
`.harness/config.json`, `tests/CMakeLists.txt`를 만든다.
- 실제 target은 `fesa_core` 정적 라이브러리와 `fesa` CLI 두 개만 만든다.
- `include/fesa/core/version.hpp`, `src/fesa/core/version.cpp`,
`src/fesa/cli/main.cpp`에 다음 계약을 구현한다.
```cpp
namespace fesa {
[[nodiscard]] std::string_view version() noexcept;
}
```
- CLI는 이 step에서 `fesa --version`만 처리한다.
- 먼저 `VersionCommand` CTest를 등록해 실패를 확인한 뒤 최소 구현한다.
- `windows-debug`, `windows-release` configure/build/test preset을 정의한다.
- configure preset은 `Visual Studio 18 2026` 생성기와 `v145` toolset, x64
architecture를 명시한다.
- build 산출물은 `out/build/<preset>` 아래에만 둔다.
- MSVC v145가 아니거나 x64가 아니면 configure 단계에서 명확히 실패시킨다.
## Acceptance Criteria
```powershell
uv run --with pytest python -m pytest -v -rs
cmake --preset windows-debug
cmake --build --preset windows-debug
ctest --preset windows-debug -R DependencySmoke --output-on-failure
ctest --preset windows-debug -R VersionCommand --output-on-failure
ctest --preset windows-debug --output-on-failure
```
네 의존성을 실제 호출하는 smoke test가 통과해야 하며 새 MSVC 경고가 없어야 한다.
`fesa --version`은 비어 있지 않은 FESA 버전을 출력하고 종료 코드 0을 반환해야 한다.
CTest는 0개가 아니어야 한다.
## 검증 절차
1. smoke test를 먼저 추가하고 link 또는 실행 실패를 확인한다.
2. dependency discovery와 target link만 최소 수정한다.
3. 전체 configure/build/test를 새로 실행한다.
4. 성공 시 정확한 imported target과 탐색 파일을 summary에 기록한다.
5. 패키지나 MSVC가 없으면 세 차례 임의 수정하지 말고 `blocked`로 종료한다.
1. 테스트가 구현 전 실패하는 것을 확인한다.
2. Acceptance Criteria 명령을 새로 실행한다.
3. Visual Studio 2026/MSVC v145/C++20/x64와 산출물 경로를 확인한다.
4. 성공 시 index의 step을 `completed`로 바꾸고 생성 파일을 summary에 기록한다.
5. 도구가 없으면 자동 설치하지 말고 `blocked`와 정확한 누락 항목을 기록한다.
## 금지사항
- FetchContent, vcpkg, Conan 또는 다운로드를 추가하지 마라. 이유: ADR-002 위반다.
- vendor 절대경로를 public header에 노출하지 마라. 이유: backend 격리를 깨뜨린다.
- solver 기능을 구현하지 마라. 이유: 이 step은 build dependency 계약만 다룬다.
- 외부 패키지를 다운로드하지 마라. 이유: 사전 설치 의존성 정책을 위반다.
- MKL, TBB, HDF5 기능을 구현하지 마라. 이유: 다음 step의 독립 범위다.
- 빈 미래 모듈을 만들지 마라. 이유: Phase 1 최소 실체화 원칙을 위반한다.
@@ -0,0 +1,8 @@
{
"step": 2,
"name": "dependency-smoke-tests",
"exitCode": 0,
"stdout": "The Harness Codex invocation produced the Step 2 source changes but exceeded its 1800-second timeout before returning. The supervising Codex recovered the step by running the documented acceptance commands in the same worktree with package locations supplied only through the build environment: cmake --preset windows-debug; cmake --build --preset windows-debug; ctest --preset windows-debug -R DependencySmoke --output-on-failure; ctest --preset windows-debug --output-on-failure. Configure and build succeeded without new MSVC warnings, DependencySmoke passed, and all 2 CTest tests passed. The 18 Harness Python self-tests also passed.",
"stderr": "Recovered after the original Harness Codex subprocess timed out; no configure, build, or test failure remained.",
"recovered": true
}
+43 -45
View File
@@ -1,4 +1,4 @@
# Step 2: Core IDs and Diagnostics
# Step 2: Dependency Smoke Tests
## 읽어야 할 파일
@@ -6,67 +6,65 @@
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/HARNESS.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/CMakeLists.txt`
- `/CMakePresets.json`
- `/cmake/FesaDependencies.cmake`
- `/.harness/config.json`
- `/tests/CMakeLists.txt`
- `/include/fesa/core/version.hpp`
이전 step의 CMake target과 preset을 그대로 확장하라.
현재 개발환경에서 검증된 package 위치:
- HDF5 2.1.1:
`C:/Program Files/HDF_Group/HDF5/2.1.1/cmake/hdf5-config.cmake`
- HDF5 runtime:
`C:/Program Files/HDF_Group/HDF5/2.1.1/bin`
- GoogleTest/GoogleMock v1.17, VS2026/MSVC v145 x64:
`C:/Users/baram/AppData/Local/FESA/dependencies/googletest-1.17.0-v145-x64-crt/lib/cmake/GTest/GTestConfig.cmake`
위 파일의 존재를 먼저 확인하고 package root를 `CMAKE_PREFIX_PATH` 또는
`HDF5_DIR`/`GTest_DIR` hint로 사용하라. 이 절대경로를 production CMake에
하드코딩하지 마라. 이유: 개발환경별 설치 위치를 build contract와 분리해야 한다.
## 작업
외부 라이브러리에 의존하지 않는 `core` 값 타입을 TDD로 구현한다.
사전 설치된 oneMKL, oneTBB, HDF5 C API, GoogleTest/GoogleMock을 CMake imported
target으로 찾고 링크 계약을 검증한다.
- 생성 파일:
`include/fesa/core/entity_id.hpp`, `vec3.hpp`, `source_location.hpp`,
`diagnostic.hpp`, `status.hpp`와 대응 테스트
- 인터페이스:
```cpp
template<class Tag>
class EntityId final {
public:
explicit constexpr EntityId(std::int64_t value);
[[nodiscard]] constexpr std::int64_t value() const noexcept;
auto operator<=>(const EntityId&) const = default;
};
struct Vec3 final { double x; double y; double z; };
struct SourceLocation final {
std::filesystem::path file;
std::size_t line;
std::size_t column;
};
enum class DiagnosticStage { io, syntax, semantic, model, equation, solver, results, validation };
enum class Severity { warning, error };
struct Diagnostic final {
DiagnosticStage stage;
Severity severity;
std::string code;
std::string message;
std::optional<SourceLocation> source;
};
```
- typed ID의 잘못된 암시 변환, 음수 ID, nonfinite vector와 diagnostic source 보존을
실패 테스트로 먼저 고정한다.
- `tests/unit/dependencies/dependency_smoke_test.cpp`를 먼저 작성한다.
- 테스트는 MKL의 작은 vector 연산, TBB의 제한된 parallel loop, HDF5 임시 파일
생성·닫기, GoogleTest 실행을 확인한다.
- `FesaDependencies.cmake``MKL::MKL`, TBB imported target, HDF5 C target,
GoogleTest target을 제공해야 한다.
- oneMKL은 LP64, dynamic link, TBB threading 조합을 사용한다.
- runtime DLL 또는 architecture 불일치는 configure diagnostic으로 보고한다.
## Acceptance Criteria
```powershell
cmake --preset windows-debug
cmake --build --preset windows-debug
ctest --preset windows-debug -R "Core|Diagnostic|EntityId" --output-on-failure
ctest --preset windows-debug -R DependencySmoke --output-on-failure
ctest --preset windows-debug --output-on-failure
```
네 의존성을 실제 호출하는 smoke test가 통과해야 하며 새 MSVC 경고가 없어야 한다.
## 검증 절차
1. production header 전에 실패하는 GoogleTest를 작성한다.
2. 최소 값 타입만 구현한다.
3. focused test와 전체 CTest를 실행한다.
4. `core`가 MKL, TBB, HDF5, Abaqus header를 include하지 않는지 확인한다.
5. index와 summary를 갱신한다.
1. smoke test를 먼저 추가하고 link 또는 실행 실패를 확인한다.
2. dependency discovery와 target link만 최소 수정한다.
3. 전체 configure/build/test를 새로 실행한다.
4. 성공 시 정확한 imported target과 탐색 파일을 summary에 기록한다.
5. 패키지나 MSVC가 없으면 세 차례 임의 수정하지 말고 `blocked`로 종료한다.
## 금지사항
- 단위 변환 시스템을 만들지 마라. 이유: FESA는 일관 단위계만 사용한다.
- 범용 reflection이나 serialization을 만들지 마라. 이유: 요구되지 않았다.
- equation ID를 정의하지 마라. 이유: `DofManager` 단계의 책임이다.
- FetchContent, vcpkg, Conan 또는 다운로드를 추가하지 마라. 이유: ADR-002 위반이다.
- vendor 절대경로를 public header에 노출하지 마라. 이유: backend 격리를 깨뜨린다.
- 현재 개발환경의 package 절대경로를 tracked CMake cache variable 기본값으로 넣지
마라. 이유: 다른 개발환경의 configure를 깨뜨린다.
- solver 기능을 구현하지 마라. 이유: 이 step은 build dependency 계약만 다룬다.
File diff suppressed because one or more lines are too long
+73
View File
@@ -0,0 +1,73 @@
# Step 3: Core IDs and Diagnostics
## 읽어야 할 파일
- `/AGENTS.md`
- `/docs/PRD.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
- `/CMakeLists.txt`
- `/tests/CMakeLists.txt`
- `/include/fesa/core/version.hpp`
- `/tests/unit/dependencies/dependency_smoke_test.cpp`
## 작업
외부 라이브러리에 의존하지 않는 `core` 값 타입을 TDD로 구현한다.
- 생성 파일:
`include/fesa/core/entity_id.hpp`, `vec3.hpp`, `source_location.hpp`,
`diagnostic.hpp`, `status.hpp`와 대응 테스트
- 인터페이스:
```cpp
template<class Tag>
class EntityId final {
public:
explicit constexpr EntityId(std::int64_t value);
[[nodiscard]] constexpr std::int64_t value() const noexcept;
auto operator<=>(const EntityId&) const = default;
};
struct Vec3 final { double x; double y; double z; };
struct SourceLocation final {
std::filesystem::path file;
std::size_t line;
std::size_t column;
};
enum class DiagnosticStage { io, syntax, semantic, model, equation, solver, results, validation };
enum class Severity { warning, error };
struct Diagnostic final {
DiagnosticStage stage;
Severity severity;
std::string code;
std::string message;
std::optional<SourceLocation> source;
};
```
- typed ID의 잘못된 암시 변환, 음수 ID, nonfinite vector와 diagnostic source 보존을
실패 테스트로 먼저 고정한다.
## Acceptance Criteria
```powershell
cmake --build --preset windows-debug
ctest --preset windows-debug -R "Core|Diagnostic|EntityId" --output-on-failure
ctest --preset windows-debug --output-on-failure
```
## 검증 절차
1. production header 전에 실패하는 GoogleTest를 작성한다.
2. 최소 값 타입만 구현한다.
3. focused test와 전체 CTest를 실행한다.
4. `core`가 MKL, TBB, HDF5, Abaqus header를 include하지 않는지 확인한다.
5. index와 summary를 갱신한다.
## 금지사항
- 단위 변환 시스템을 만들지 마라. 이유: FESA는 일관 단위계만 사용한다.
- 범용 reflection이나 serialization을 만들지 마라. 이유: 요구되지 않았다.
- equation ID를 정의하지 마라. 이유: `DofManager` 단계의 책임이다.
+132 -18
View File
@@ -3,13 +3,14 @@
Harness Step Executor phase step을 순차 실행하고 자가 교정한다.
Usage:
python scripts/execute.py <phase-dir> [--push]
python scripts/execute.py <phase-dir> [--push] [--codex-add-dir DIR]
"""
import argparse
import contextlib
import json
import os
import shutil
import subprocess
import sys
import threading
@@ -26,6 +27,78 @@ class CodexEnvironmentError(RuntimeError):
"""재시도로 해결할 수 없는 Codex CLI 환경 오류."""
def normalize_codex_add_dirs(raw_dirs) -> tuple[Path, ...]:
"""검증된 Codex 추가 writable directory를 반환한다."""
normalized = []
for raw in raw_dirs:
candidate = Path(raw)
if not candidate.is_absolute():
raise CodexEnvironmentError(
f"Codex 추가 경로는 절대 경로여야 합니다: {raw}"
)
resolved = candidate.resolve()
if not resolved.is_dir():
raise CodexEnvironmentError(
f"Codex 추가 경로가 존재하는 디렉터리가 아닙니다: {resolved}"
)
normalized.append(resolved)
return tuple(normalized)
def build_codex_command(
codex_executable: Path,
root: Path,
add_dirs: tuple[Path, ...],
) -> list[str]:
"""workspace-write Codex command를 안전한 argv로 구성한다."""
command = [
str(codex_executable.resolve()),
"exec",
"--json",
"--sandbox",
"workspace-write",
"--dangerously-bypass-hook-trust",
]
for path in add_dirs:
command.extend(("--add-dir", str(path)))
command.extend(("--cd", str(root.resolve()), "-"))
return command
def build_codex_environment(
base_env,
add_dirs: tuple[Path, ...],
) -> dict[str, str]:
"""명시적 tool directory를 기존 PATH 뒤에 추가한다."""
environment = dict(base_env)
path_entries = [environment.get("PATH", "")]
path_entries.extend(str(path) for path in add_dirs)
environment["PATH"] = os.pathsep.join(entry for entry in path_entries if entry)
return environment
def run_utf8_process(command, *, cwd, prompt, env, timeout):
"""UTF-8 prompt/output 계약으로 subprocess를 실행한다."""
return subprocess.run(
list(command),
cwd=cwd,
input=prompt,
capture_output=True,
encoding="utf-8",
errors="replace",
env=env,
timeout=timeout,
)
def configure_standard_streams(*streams) -> None:
"""Windows legacy console에서 status 출력이 중단되지 않게 한다."""
for stream in streams:
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is not None:
reconfigure(errors="replace")
@contextlib.contextmanager
def progress_indicator(label: str):
"""터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다."""
@@ -62,13 +135,20 @@ class StepExecutor:
CHORE_MSG = "chore({phase}): step {num} output"
TZ = timezone(timedelta(hours=9))
def __init__(self, phase_dir_name: str, *, auto_push: bool = False):
def __init__(
self,
phase_dir_name: str,
*,
auto_push: bool = False,
codex_add_dirs=(),
):
self._root = str(ROOT)
self._phases_dir = ROOT / "phases"
self._phase_dir = self._phases_dir / phase_dir_name
self._phase_dir_name = phase_dir_name
self._top_index_file = self._phases_dir / "index.json"
self._auto_push = auto_push
self._codex_add_dirs = normalize_codex_add_dirs(codex_add_dirs)
if not self._phase_dir.is_dir():
print(f"ERROR: {self._phase_dir} not found")
@@ -241,26 +321,45 @@ class StepExecutor:
sys.exit(1)
prompt = preamble + step_file.read_text(encoding="utf-8")
command = [
"codex",
"exec",
"--json",
"--sandbox",
"workspace-write",
"--dangerously-bypass-hook-trust",
"--cd",
self._root,
"-",
]
codex_executable = shutil.which("codex")
if codex_executable is None:
raise CodexEnvironmentError(
"Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요."
)
command = build_codex_command(
Path(codex_executable),
Path(self._root),
self._codex_add_dirs,
)
environment = build_codex_environment(
os.environ,
self._codex_add_dirs,
)
try:
result = subprocess.run(
result = run_utf8_process(
command,
cwd=self._root,
input=prompt,
capture_output=True,
text=True,
prompt=prompt,
env=environment,
timeout=1800,
)
except subprocess.TimeoutExpired as exc:
stdout = exc.stdout or ""
stderr = exc.stderr or ""
if isinstance(stdout, bytes):
stdout = stdout.decode("utf-8", errors="replace")
if isinstance(stderr, bytes):
stderr = stderr.decode("utf-8", errors="replace")
timeout_message = (
f"Codex timed out after {exc.timeout} seconds."
)
stderr = f"{stderr}\n{timeout_message}".strip()
result = subprocess.CompletedProcess(
command,
returncode=124,
stdout=stdout,
stderr=stderr,
)
except FileNotFoundError as exc:
raise CodexEnvironmentError(
"Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요."
@@ -465,13 +564,28 @@ class StepExecutor:
def main():
configure_standard_streams(sys.stdout, sys.stderr)
parser = argparse.ArgumentParser(description="Harness Step Executor")
parser.add_argument("phase_dir", help="Phase directory name (e.g. 0-mvp)")
parser.add_argument("--push", action="store_true", help="Push branch after completion")
parser.add_argument(
"--codex-add-dir",
action="append",
default=[],
metavar="DIR",
help=(
"Codex workspace-write sandbox에 추가할 절대 tool/runtime directory. "
"여러 번 지정할 수 있습니다."
),
)
args = parser.parse_args()
try:
StepExecutor(args.phase_dir, auto_push=args.push).run()
StepExecutor(
args.phase_dir,
auto_push=args.push,
codex_add_dirs=args.codex_add_dir,
).run()
except CodexEnvironmentError as exc:
print(f"ERROR: {exc}")
sys.exit(1)
+14
View File
@@ -0,0 +1,14 @@
#include <fesa/core/version.hpp>
#include <iostream>
#include <string_view>
int main(int argc, char* argv[]) {
if (argc == 2 && std::string_view{argv[1]} == "--version") {
std::cout << fesa::version() << '\n';
return 0;
}
std::cerr << "Usage: fesa --version\n";
return 1;
}
+9
View File
@@ -0,0 +1,9 @@
#include <fesa/core/version.hpp>
namespace fesa {
std::string_view version() noexcept {
return "0.1.0";
}
}
+77
View File
@@ -0,0 +1,77 @@
add_test(
NAME VersionCommand
COMMAND "${CMAKE_BINARY_DIR}/$<CONFIG>/fesa.exe" --version
)
set_tests_properties(
VersionCommand
PROPERTIES
PASS_REGULAR_EXPRESSION "[0-9]+\\.[0-9]+\\.[0-9]+"
)
add_executable(fesa_dependency_smoke_test
unit/dependencies/dependency_smoke_test.cpp
)
target_compile_features(fesa_dependency_smoke_test PRIVATE cxx_std_20)
target_compile_options(fesa_dependency_smoke_test PRIVATE /W4 /permissive- /EHsc)
target_link_libraries(fesa_dependency_smoke_test
PRIVATE
MKL::MKL
TBB::tbb
HDF5::HDF5
GTest::gtest_main
GTest::gtest
GTest::gmock
)
add_test(
NAME DependencySmoke
COMMAND "$<TARGET_FILE:fesa_dependency_smoke_test>"
)
set_property(
TEST DependencySmoke
PROPERTY ENVIRONMENT_MODIFICATION
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
)
add_test(
NAME ProductionConfigureWithoutGTest
COMMAND
"${CMAKE_COMMAND}"
--fresh
-S "${CMAKE_SOURCE_DIR}"
-B "${CMAKE_BINARY_DIR}/testing/production-no-gtest"
-G "${CMAKE_GENERATOR}"
-A "${CMAKE_GENERATOR_PLATFORM}"
-T "${CMAKE_VS_PLATFORM_TOOLSET}"
-DBUILD_TESTING=OFF
-DCMAKE_DISABLE_FIND_PACKAGE_GTest=TRUE
"-DMKL_DIR=${MKL_DIR}"
"-DTBB_DIR=${TBB_DIR}"
"-DHDF5_DIR=${HDF5_DIR}"
)
add_executable(fesa_core_value_tests
unit/core/diagnostic_test.cpp
unit/core/entity_id_test.cpp
unit/core/status_test.cpp
unit/core/vec3_test.cpp
unit/core/version_test.cpp
)
target_compile_features(fesa_core_value_tests PRIVATE cxx_std_20)
target_compile_options(fesa_core_value_tests PRIVATE /W4 /permissive- /EHsc)
target_link_libraries(fesa_core_value_tests
PRIVATE
fesa_core
GTest::gtest_main
)
add_test(
NAME CoreValueTypes
COMMAND "$<TARGET_FILE:fesa_core_value_tests>"
)
+68
View File
@@ -0,0 +1,68 @@
import json
import pytest
from scripts.msvc_harness.config import ConfigError, load_config
def _write_config(root, data):
config_dir = root / ".harness"
config_dir.mkdir()
(config_dir / "config.json").write_text(
json.dumps(data),
encoding="utf-8",
)
def test_load_config_uses_defaults(tmp_path):
config = load_config(tmp_path)
assert config.version == 1
assert config.project_type == "auto"
assert config.cmake.source_dir == tmp_path.resolve()
assert config.cmake.binary_dir is None
assert config.tdd.test_roots == (
(tmp_path / "tests").resolve(),
(tmp_path / "test").resolve(),
)
def test_load_config_rejects_path_outside_repository(tmp_path):
root = tmp_path / "repo"
root.mkdir()
_write_config(
root,
{
"version": 1,
"cmake": {
"sourceDir": "../outside",
},
},
)
with pytest.raises(ConfigError, match="outside the repository"):
load_config(root)
@pytest.mark.parametrize(
"missing_field",
["binaryDir", "configurePreset", "buildPreset", "testPreset"],
)
def test_load_config_requires_all_cmake_preset_fields(tmp_path, missing_field):
cmake = {
"binaryDir": "out/build/windows-debug",
"configurePreset": "windows-debug",
"buildPreset": "windows-debug",
"testPreset": "windows-debug",
}
del cmake[missing_field]
_write_config(
tmp_path,
{
"version": 1,
"cmake": cmake,
},
)
with pytest.raises(ConfigError, match="must be specified together"):
load_config(tmp_path)
+30
View File
@@ -0,0 +1,30 @@
import pytest
from scripts.msvc_harness.config import load_config
from scripts.msvc_harness.discovery import DiscoveryError, discover_project
from scripts.msvc_harness.models import ProjectKind
def test_discover_project_skips_empty_repository(tmp_path):
result = discover_project(tmp_path, load_config(tmp_path))
assert result.selection is None
def test_discover_project_prefers_cmake(tmp_path):
cmake_lists = tmp_path / "CMakeLists.txt"
cmake_lists.write_text("cmake_minimum_required(VERSION 3.25)\n", encoding="utf-8")
(tmp_path / "fallback.sln").write_text("", encoding="utf-8")
result = discover_project(tmp_path, load_config(tmp_path))
assert result.selection is not None
assert result.selection.kind is ProjectKind.CMAKE
assert result.selection.project_file == cmake_lists
def test_discover_project_rejects_orphan_cpp_project(tmp_path):
(tmp_path / "orphan.cpp").write_text("int value = 0;\n", encoding="utf-8")
with pytest.raises(DiscoveryError, match="C/C\\+\\+ files exist"):
discover_project(tmp_path, load_config(tmp_path))
+214
View File
@@ -0,0 +1,214 @@
import os
import subprocess
import sys
from pathlib import Path
import pytest
import scripts.execute as execute_module
from scripts.execute import (
CodexEnvironmentError,
StepExecutor,
build_codex_command,
build_codex_environment,
configure_standard_streams,
normalize_codex_add_dirs,
run_utf8_process,
)
def test_normalize_codex_add_dirs_accepts_existing_absolute_directories(tmp_path):
first = tmp_path / "tool"
second = tmp_path / "runtime"
first.mkdir()
second.mkdir()
result = normalize_codex_add_dirs([str(first), str(second)])
assert result == (first.resolve(), second.resolve())
@pytest.mark.parametrize("raw", ["relative/tool", "missing"])
def test_normalize_codex_add_dirs_rejects_unsafe_paths(tmp_path, raw):
candidate = raw if raw.startswith("relative") else str(tmp_path / raw)
with pytest.raises(CodexEnvironmentError):
normalize_codex_add_dirs([candidate])
def test_build_codex_command_grants_only_explicit_directories(tmp_path):
root = tmp_path / "repo"
tool = tmp_path / "tool"
runtime = tmp_path / "runtime"
root.mkdir()
tool.mkdir()
runtime.mkdir()
codex = tmp_path / "codex.exe"
command = build_codex_command(
codex,
root,
(tool.resolve(), runtime.resolve()),
)
assert command == [
str(codex),
"exec",
"--json",
"--sandbox",
"workspace-write",
"--dangerously-bypass-hook-trust",
"--add-dir",
str(tool.resolve()),
"--add-dir",
str(runtime.resolve()),
"--cd",
str(root.resolve()),
"-",
]
def test_build_codex_environment_appends_tool_directories(tmp_path):
tool = (tmp_path / "tool").resolve()
runtime = (tmp_path / "runtime").resolve()
base = {"PATH": os.pathsep.join(("existing-one", "existing-two")), "KEEP": "value"}
result = build_codex_environment(base, (tool, runtime))
assert result["PATH"].split(os.pathsep) == [
"existing-one",
"existing-two",
str(tool),
str(runtime),
]
assert result["KEEP"] == "value"
assert result is not base
def test_run_utf8_process_sends_prompt_as_utf8(tmp_path):
verifier = tmp_path / "verify_utf8.py"
verifier.write_text(
"import sys\n"
"payload = sys.stdin.buffer.read()\n"
"text = payload.decode('utf-8')\n"
"sys.stdout.buffer.write(text.encode('utf-8'))\n",
encoding="utf-8",
)
prompt = "FESA 한글 prompt"
result = run_utf8_process(
[sys.executable, str(verifier)],
cwd=tmp_path,
prompt=prompt,
env=os.environ.copy(),
timeout=10,
)
assert result.returncode == 0
assert result.stdout == prompt
def test_invoke_codex_records_timeout_with_partial_output(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "test-phase"
phase_dir.mkdir(parents=True)
(phase_dir / "step0.md").write_text("# Timed step\n", encoding="utf-8")
codex = tmp_path / "codex.exe"
codex.touch()
executor = StepExecutor.__new__(StepExecutor)
executor._root = str(tmp_path)
executor._phase_dir = phase_dir
executor._codex_add_dirs = ()
monkeypatch.setattr(
execute_module.shutil, "which", lambda executable: str(codex)
)
def raise_timeout(*args, **kwargs):
raise subprocess.TimeoutExpired(
cmd=["codex", "exec"],
timeout=1800,
output=b"partial stdout",
stderr=b"partial stderr",
)
monkeypatch.setattr(execute_module, "run_utf8_process", raise_timeout)
output = executor._invoke_codex(
{"step": 0, "name": "timed-step"}, "preamble\n"
)
assert output["exitCode"] == 124
assert output["stdout"] == "partial stdout"
assert "partial stderr" in output["stderr"]
assert "timed out after 1800 seconds" in output["stderr"]
assert (
execute_module.json.loads(
(phase_dir / "step0-output.json").read_text(encoding="utf-8")
)
== output
)
def test_execute_single_step_retries_timeout_three_times(tmp_path, monkeypatch):
index_file = tmp_path / "index.json"
index_file.write_text(
execute_module.json.dumps(
{
"steps": [
{"step": 0, "name": "timed-step", "status": "pending"}
]
}
),
encoding="utf-8",
)
executor = StepExecutor.__new__(StepExecutor)
executor._index_file = index_file
executor._total = 1
executor._project = "FESA"
executor._phase_dir_name = "test-phase"
attempts = 0
def timeout_output(step, preamble):
nonlocal attempts
attempts += 1
return {
"step": 0,
"name": "timed-step",
"exitCode": 124,
"stdout": "",
"stderr": "Codex timed out after 1800 seconds.",
}
monkeypatch.setattr(executor, "_invoke_codex", timeout_output)
monkeypatch.setattr(executor, "_commit_step", lambda *args: None)
monkeypatch.setattr(executor, "_update_top_index", lambda *args: None)
with pytest.raises(SystemExit) as exit_info:
executor._execute_single_step(
{"step": 0, "name": "timed-step", "status": "pending"}, ""
)
assert exit_info.value.code == 1
assert attempts == 3
index = execute_module.json.loads(index_file.read_text(encoding="utf-8"))
assert index["steps"][0]["status"] == "error"
assert "timed out after 1800 seconds" in index["steps"][0]["error_message"]
def test_configure_standard_streams_replaces_unencodable_status_characters():
class RecordingStream:
def __init__(self):
self.calls = []
def reconfigure(self, **kwargs):
self.calls.append(kwargs)
stdout = RecordingStream()
stderr = RecordingStream()
configure_standard_streams(stdout, stderr)
assert stdout.calls == [{"errors": "replace"}]
assert stderr.calls == [{"errors": "replace"}]
+74
View File
@@ -0,0 +1,74 @@
from types import SimpleNamespace
import pytest
from scripts.msvc_harness.models import (
CommandSpec,
ProjectKind,
ResultCheck,
ResultCheckKind,
ValidationPlan,
ValidationStep,
)
from scripts.msvc_harness.process import ValidationFailure, execute_plan
def _ctest_plan(root):
command = CommandSpec(
("ctest", "--show-only=json-v1"),
root,
"CTest discovery",
)
check = ResultCheck(ResultCheckKind.CTEST_HAS_TESTS)
return ValidationPlan(
ProjectKind.CMAKE,
(ValidationStep(command, (check,)),),
)
def test_execute_plan_rejects_ctest_json_with_no_tests(tmp_path):
def fake_run(*args, **kwargs):
return SimpleNamespace(returncode=0, stdout='{"tests": []}', stderr="")
with pytest.raises(ValidationFailure, match="CTest discovered no tests"):
execute_plan(
_ctest_plan(tmp_path),
tmp_path,
deadline=10,
run=fake_run,
clock=lambda: 0,
)
def test_execute_plan_accepts_ctest_json_with_tests_without_shell(tmp_path):
calls = []
def fake_run(*args, **kwargs):
calls.append((args, kwargs))
return SimpleNamespace(
returncode=0,
stdout='{"tests": [{"name": "unit"}]}',
stderr="",
)
results = execute_plan(
_ctest_plan(tmp_path),
tmp_path,
deadline=10,
run=fake_run,
clock=lambda: 0,
)
assert len(results) == 1
assert calls == [
(
(["ctest", "--show-only=json-v1"],),
{
"cwd": tmp_path.resolve(),
"env": None,
"capture_output": True,
"shell": False,
"timeout": 10,
},
)
]
+38
View File
@@ -0,0 +1,38 @@
#include <filesystem>
#include <optional>
#include <string>
#include <gtest/gtest.h>
#include <fesa/core/diagnostic.hpp>
namespace {
TEST(Diagnostic, PreservesSourceLocation) {
const fesa::Diagnostic diagnostic{
fesa::DiagnosticStage::syntax,
fesa::Severity::error,
"ABAQUS_INVALID_NODE",
"Node data is incomplete.",
fesa::SourceLocation{
std::filesystem::path{"models/beam.inp"}, 42, 9}};
ASSERT_TRUE(diagnostic.source.has_value());
EXPECT_EQ(
diagnostic.source->file, std::filesystem::path{"models/beam.inp"});
EXPECT_EQ(diagnostic.source->line, 42U);
EXPECT_EQ(diagnostic.source->column, 9U);
}
TEST(Diagnostic, AllowsDiagnosticsWithoutSourceLocation) {
const fesa::Diagnostic diagnostic{
fesa::DiagnosticStage::solver,
fesa::Severity::warning,
"SOLVER_RESIDUAL",
"Residual is above the reporting threshold.",
std::nullopt};
EXPECT_FALSE(diagnostic.source.has_value());
}
} // namespace
+36
View File
@@ -0,0 +1,36 @@
#include <cstdint>
#include <stdexcept>
#include <type_traits>
#include <gtest/gtest.h>
#include <fesa/core/entity_id.hpp>
namespace {
struct NodeTag;
struct ElementTag;
using NodeId = fesa::EntityId<NodeTag>;
using ElementId = fesa::EntityId<ElementTag>;
static_assert(std::is_constructible_v<NodeId, std::int64_t>);
static_assert(!std::is_convertible_v<std::int64_t, NodeId>);
static_assert(!std::is_convertible_v<NodeId, ElementId>);
TEST(EntityId, PreservesValueAndSupportsTypedOrdering) {
constexpr NodeId first{7};
constexpr NodeId second{11};
static_assert(first.value() == 7);
static_assert(first < second);
EXPECT_EQ(first.value(), 7);
EXPECT_LT(first, second);
}
TEST(EntityId, RejectsNegativeValues) {
EXPECT_THROW((NodeId{-1}), std::invalid_argument);
}
} // namespace
+35
View File
@@ -0,0 +1,35 @@
#include <filesystem>
#include <vector>
#include <gtest/gtest.h>
#include <fesa/core/status.hpp>
namespace {
TEST(CoreStatus, PreservesFailureDiagnostics) {
const fesa::Status status{
false,
{fesa::Diagnostic{
fesa::DiagnosticStage::model,
fesa::Severity::error,
"MODEL_NONFINITE_VECTOR",
"A vector component is not finite.",
fesa::SourceLocation{
std::filesystem::path{"models/invalid.inp"}, 8, 3}}}};
EXPECT_FALSE(status.succeeded);
ASSERT_EQ(status.diagnostics.size(), 1U);
EXPECT_EQ(status.diagnostics.front().code, "MODEL_NONFINITE_VECTOR");
ASSERT_TRUE(status.diagnostics.front().source.has_value());
EXPECT_EQ(status.diagnostics.front().source->line, 8U);
}
TEST(CoreStatus, RepresentsSuccessWithoutDiagnostics) {
const fesa::Status status{true, {}};
EXPECT_TRUE(status.succeeded);
EXPECT_TRUE(status.diagnostics.empty());
}
} // namespace
+20
View File
@@ -0,0 +1,20 @@
#include <limits>
#include <gtest/gtest.h>
#include <fesa/core/vec3.hpp>
namespace {
TEST(CoreVec3, ReportsOnlyFiniteVectorsAsFinite) {
EXPECT_TRUE(fesa::is_finite(fesa::Vec3{1.0, -2.0, 0.0}));
const double infinity = std::numeric_limits<double>::infinity();
const double nan = std::numeric_limits<double>::quiet_NaN();
EXPECT_FALSE(fesa::is_finite(fesa::Vec3{infinity, 0.0, 0.0}));
EXPECT_FALSE(fesa::is_finite(fesa::Vec3{0.0, -infinity, 0.0}));
EXPECT_FALSE(fesa::is_finite(fesa::Vec3{0.0, 0.0, nan}));
}
} // namespace
+11
View File
@@ -0,0 +1,11 @@
#include <gtest/gtest.h>
#include <fesa/core/version.hpp>
namespace {
TEST(Version, ReportsPhaseOneSemanticVersion) {
EXPECT_EQ(fesa::version(), "0.1.0");
}
} // namespace
@@ -0,0 +1,56 @@
#include <array>
#include <cstddef>
#include <filesystem>
#include <system_error>
#include <gmock/gmock.h>
#include <hdf5.h>
#include <mkl.h>
#include <oneapi/tbb/global_control.h>
#include <oneapi/tbb/parallel_for.h>
namespace {
static_assert(sizeof(MKL_INT) == 4, "FESA requires the oneMKL LP64 interface.");
TEST(DependencySmoke, CallsOneMklVectorOperation) {
constexpr std::array<double, 3> x{1.0, 2.0, 3.0};
std::array<double, 3> y{4.0, 5.0, 6.0};
cblas_daxpy(
static_cast<MKL_INT>(x.size()), 2.0, x.data(), 1, y.data(), 1);
EXPECT_THAT(y, ::testing::ElementsAre(6.0, 9.0, 12.0));
}
TEST(DependencySmoke, RunsLimitedOneTbbParallelLoop) {
std::array<std::size_t, 16> values{};
const oneapi::tbb::global_control limit{
oneapi::tbb::global_control::max_allowed_parallelism, 2};
oneapi::tbb::parallel_for(
std::size_t{0}, values.size(), [&values](const std::size_t index) {
values[index] = index + 1;
});
EXPECT_THAT(values, ::testing::ElementsAre(
1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U,
9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U));
}
TEST(DependencySmoke, CreatesAndClosesHdf5File) {
const auto path =
std::filesystem::path{::testing::TempDir()} /
"fesa_dependency_smoke.h5";
const hid_t file = H5Fcreate(
path.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
ASSERT_GE(file, 0);
EXPECT_GE(H5Fclose(file), 0);
std::error_code error;
std::filesystem::remove(path, error);
EXPECT_FALSE(error);
}
} // namespace