From ac84235b7a21729962f817c3485b58e4e0296bff Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 6 Sep 2026 00:52:59 +0900 Subject: [PATCH] modify documents --- .agents/skills/harness/SKILL.md | 50 +- .codex/agents/implementation-agent.toml | 4 + .../agents/implementation-planning-agent.toml | 4 + AGENTS.md | 5 +- docs/ADR.md | 587 ++++++++------- docs/ARCHITECTURE.md | 669 ++++++------------ docs/CODINGSTYLE.md | 2 +- docs/HARNESS.md | 81 ++- docs/HARNESS_WORKFLOW.md | 217 ++++-- docs/PRD.md | 289 ++++---- docs/archive/pre-generalization/ADR.md | 375 ++++++++++ .../pre-generalization/ARCHITECTURE.md | 506 +++++++++++++ docs/archive/pre-generalization/PRD.md | 179 +++++ scripts/execute.py | 300 +++++--- tests/test_agent_skill_workflow_contract.py | 185 ++--- tests/test_execute.py | 306 +++++++- tests/test_execute_ownership.py | 348 +++++++++ 17 files changed, 2912 insertions(+), 1195 deletions(-) create mode 100644 docs/archive/pre-generalization/ADR.md create mode 100644 docs/archive/pre-generalization/ARCHITECTURE.md create mode 100644 docs/archive/pre-generalization/PRD.md create mode 100644 tests/test_execute_ownership.py diff --git a/.agents/skills/harness/SKILL.md b/.agents/skills/harness/SKILL.md index 83d276b..2d23818 100644 --- a/.agents/skills/harness/SKILL.md +++ b/.agents/skills/harness/SKILL.md @@ -34,9 +34,11 @@ Separate explicit user authorization is still required immediately before: | Implementation Agent | only the Executor-selected Step, its code/tests/reports, and Step result payload | | Hooks | pre-edit command/TDD guard and configured whole-project Stop validation | -The Executor owns at most two process/session transport attempts. They do not consume or reset the -Coordinator's one-correction workflow budget. An implementation, upstream-contract, environment, or -physics failure is returned for Coordinator classification rather than automatically looped. +The Executor permits at most two transport attempts, including the initial attempt, only when its +process wrapper observes a timeout and the Step has not reported a terminal result. A nonzero exit +(including 124) alone is not timeout evidence. Reported `error`/`blocked` results are preserved without +retry even on nonzero exit. Recognized sandbox-helper failures block even on zero exit. These attempts +do not consume or reset the Coordinator's one-correction workflow budget. ## Plan materialization @@ -63,11 +65,22 @@ The phase index is: "project": "", "phase": "", "steps": [ - {"step": 0, "name": "", "status": "pending"} + { + "step": 0, "name": "", "status": "pending", + "owned_paths": ["src/example.cpp", "tests/example_test.cpp"] + } ] } ``` +Replace the example paths with the Step's approved source/header/test/build/report files. `owned_paths` +is a Planning-owned array of exact repository-relative files, not directories or globs. `[]` permits +no implementation-file changes; it is not equivalent to a missing field. Do not include Git internals +or `phases/` control files. Paths cannot escape through traversal, symlinks or junctions; linked paths +are not supported. Backslashes are normalized and Windows case aliases are treated as the same path. +Existing completed Steps remain readable without this field. Every pending Step must have a valid +list before execution starts; do not infer it from Markdown or automatically migrate historical phases. + Use contiguous Step numbers from zero. A Step must be small enough to complete `RED -> observed failure -> minimal GREEN -> focused VERIFY -> full VERIFY` in one Codex session; do not leave a red-only Step for a later Step. @@ -105,16 +118,26 @@ The Implementation Agent updates only the current Step: - `error` plus `error_message` - `blocked` plus `blocked_reason` -The Executor adds timestamps. It commits implementation paths and phase metadata separately, ignores raw -`stepN-output.json`, normalizes the index before path-scoped staging, and never stages changes outside the -declared ownership boundary. Top/phase control metadata and non-current Steps are immutable during a +The Executor adds timestamps and immutable `attempt_history` records preserving reported outcomes, +process exits, classifications and diagnostics across retries/recovery. It checks changed paths after +each attempt and before staging, then checks the actual staged paths before committing. The trusted +`owned_paths` snapshot covers tracked/untracked, staged/unstaged changes and both rename endpoints. +Implementation paths and phase metadata are committed separately; raw `stepN-output.json` stays ignored. +An ownership violation stops retry/commit/advancement, records the offending paths, and preserves user +files without reverting or expanding the allowlist. Avoid concurrent edits: these checks are not a +filesystem sandbox or an atomic guarantee against concurrent writers. Commit groups are not one +transaction; a failure after the implementation commit can leave that commit with an error Step. +Top/phase control metadata and non-current Steps are immutable during a session; a violation is restored to the trusted snapshot and recorded as an error. The implementation session may not run commit, push, branch, tag, merge/rebase, or history-changing Git commands. Executor verifies symbolic branch and HEAD around every attempt. Control reads reject links and scope escape; control/output writes use atomic replacement so a substituted link target is not overwritten. Do not run hook scripts manually as substitutes for registered hooks. A PreToolUse pass proves neither -RED nor correctness; a Stop pass proves only the configured build/test gate. +RED nor correctness. Stop may skip build/test only for a valid same-session/turn record containing solely +repository-local `.md` edits. Missing/corrupt records or non-document edits take the normal validation path; +shell writes and read-only requests without edit records are not guaranteed to skip. A skipped check is not +build/test evidence. Do not manipulate hook state to obtain a skip. ## Elevated and remote execution @@ -139,8 +162,15 @@ python scripts/execute.py --push Do not manually erase failure evidence and immediately rerun. Return the exact command/exit, classification, artifact paths and `retry_key` to Coordinator. After the owning upstream/environment/user decision is -resolved, Coordinator may reset the current Step to `pending`; stale summary/failure/block timestamps are -cleared by the Executor. +resolved, Coordinator records the retry decision and preserved failure evidence in `coordination.md`, then +explicitly assigns the Planning Agent a recovery edit of the phase index. With the Executor stopped, that +agent changes only the current failed/blocked Step's status to `pending`; it does not alter other Steps, +top-level phase state, timestamps, or the feature workflow. Coordinator does not edit phase files itself. +The Executor clears stale summary/failure/block fields and timestamps on restart, retaining +`attempt_history`. For a legacy Step missing `owned_paths`, Planning needs a separate explicit assignment +to supply the approved list before execution; the status-only recovery edit does not grant that change. +This is an assigned +file edit, not an automatic recovery command or an additional correction allowance. Report the final phase state, commits created, commands and exit codes, test counts, ignored/untracked raw output, unexecuted checks and residual risks. Never call a phase complete when any Step is non-completed. diff --git a/.codex/agents/implementation-agent.toml b/.codex/agents/implementation-agent.toml index b61d1f8..9f82acc 100644 --- a/.codex/agents/implementation-agent.toml +++ b/.codex/agents/implementation-agent.toml @@ -25,6 +25,10 @@ Ownership and write boundary: - Return exactly the common handoff envelope to Coordinator; never transition the workflow or dispatch peers. Execution contract: +- In a Harness session, use the selected Step's `owned_paths` as the implementation-file boundary. + Treat that list and Executor-owned `attempt_history` as immutable. Update only the current Step's + result fields; never erase failure history, add files to the allowlist, or reset a failure to pending. + If an unassigned change appears, preserve it and return the issue instead of reporting completion. - Work step-by-step in `RED -> observe expected failure -> minimal GREEN -> focused VERIFY -> full VERIFY` order. - Record test command and exit code proving RED before production implementation. Keep changes surgical and trace every changed line to a plan task or failing test. diff --git a/.codex/agents/implementation-planning-agent.toml b/.codex/agents/implementation-planning-agent.toml index 5930246..08d1711 100644 --- a/.codex/agents/implementation-planning-agent.toml +++ b/.codex/agents/implementation-planning-agent.toml @@ -23,6 +23,10 @@ Ownership and write boundary: - Return exactly the common handoff envelope to Coordinator; never transition the workflow or dispatch peers. Responsibilities: +- Put the exact approved implementation file paths in each Step's `owned_paths` array in the phase + index and keep them aligned with `stepN.md`. Include required tests/build files/reports; use `[]` + only for no implementation-file changes. Do not use directories, globs, Git internals or phase + control files. Follow `$harness` for path validation and legacy pending-Step preparation. - Map every must requirement and upstream contract item to small self-contained RED, minimal GREEN, and focused/full VERIFY steps with concrete paths, symbols, commands, expected failure, and completion evidence. - Include configured compiler/build/test backend integration, negative parser/I/O tests, numerical invariants, deterministic diff --git a/AGENTS.md b/AGENTS.md index e002086..3b02cfb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,7 +86,7 @@ ### 선형 정적 해석 불변식 -- `LinearStaticAnalysis::Run()`의 실제 단계와 객체별 책임은 `docs/ARCHITECTURE.md`의 해석 실행 흐름을 따른다. +- 해석 실행의 공통 책임은 `docs/ARCHITECTURE.md`를 따른다. `LinearStaticAnalysis::Run()`의 구체적인 단계와 객체별 책임은 해당 기능의 승인된 계약과 구현을 대조한다. - Full stiffness를 stable free/constrained 순서로 `Kff/Kfc/Kcf/Kcc`에 분할하고, `Kff` factorization을 load assembly보다 먼저 수행한다. - Effective RHS는 `Ff - Kfc * dc`이며 substitution 뒤 full displacement를 복구한다. 모든 DOF가 constrained인 유효한 모델의 `0 x 0 Kff`를 singular error로 바꾸지 않는다. - Reaction과 free-equilibrium evidence는 조립된 full residual `K*d - F`에서 구한다. Constrained 성분은 physical reaction이고 free 성분은 residual evidence다. @@ -98,7 +98,8 @@ - Codex custom agent의 `model_reasoning_effort`는 `xhigh`이며, 모델과 역할별 설정은 [.codex/agents](.codex/agents)의 현재 profile을 따른다. - Harness runner는 [scripts/execute.py](scripts/execute.py)이며 `feat-` branch prefix를 사용한다. 실행 전 Git top-level이 Harness root와 같아야 하고, 선택된 phase 제어 파일 allowlist 밖의 worktree는 clean이어야 한다. 관련 없는 변경을 stage하거나 숨기지 않는다. - Executor는 branch, 현재 pending step 선택, transport attempt, timestamp, path-scoped staging/commit과 phase 진행을 소유한다. 구현 경로와 phase metadata를 분리해 commit하며 저장소 전체를 무조건 stage하지 않는다. Implementation Agent는 선택된 step의 코드·테스트·보고서와 result payload만 갱신하며 branch/commit/push나 다른 step·제어 metadata를 변경하지 않는다. -- Executor의 process/session transport 시도는 최대 2회이며 Coordinator의 correction budget과 별개다. 구현·상위 계약·환경·물리 실패를 transport retry로 반복하지 않는다. +- Planning은 각 Step의 승인된 정확한 상대 파일 목록을 phase index의 `owned_paths`에 기록한다. 모든 pending Step은 실행 전에 목록이 필요하고 `[]`는 구현 파일 변경 없음을 뜻한다. Executor는 신뢰된 목록으로 매 시도 후·stage 전·commit 전 경로를 검사하며 rename 양쪽 경로도 승인되어야 한다. 범위 밖 변경은 보존하고 재시도·커밋·다음 Step 진행을 중단한다. 실행 중 동시 편집까지 원자적으로 격리하는 보장은 아니다. +- Executor의 process/session transport 시도는 실제 wrapper timeout과 결과 미보고가 확인된 경우에만 최초 포함 최대 2회이며 Coordinator의 correction budget과 별개다. 비정상 종료에서도 명시적 `error/blocked`를 보존하고, 인식된 sandbox helper 환경 실패는 종료 코드 0에서도 `blocked`로 기록한다. Executor 소유 `attempt_history`는 재시도·재개 때 보존하며 구현·상위 계약·환경·물리 실패를 transport retry로 반복하지 않는다. - 기본 sandbox는 `FEM_HARNESS_CODEX_SANDBOX=workspace-write`다. `danger-full-access`는 동일 명령의 native sandbox 실패 증거, 해당 실행 직전 사용자의 명시적 승인, clean linked Git worktree와 `--approve-danger-full-access`가 모두 필요하다. 환경변수 설정만으로 승인되지 않는다. `--push` 등 원격 작업도 별도 명시적 승인이 필요하다. - Hook 연결은 [.codex/hooks.json](.codex/hooks.json), 구현은 [scripts/hooks](scripts/hooks)와 [scripts/msvc_harness](scripts/msvc_harness)에 둔다. Hook script를 수동 실행해 등록된 hook이나 검증 evidence를 대체하지 않는다. - PreToolUse는 위험 명령과 C++ production file의 대응 테스트 존재 여부를 검사하고 허용된 편집 경로를 기록한다. 이는 RED 실행이나 correctness의 증거가 아니다. diff --git a/docs/ADR.md b/docs/ADR.md index 4ffd9b1..6382413 100644 --- a/docs/ADR.md +++ b/docs/ADR.md @@ -1,375 +1,346 @@ -# Architecture Decision Records +# FESA Architecture Decision Records -## 철학 -FESA의 architecture decision은 solver correctness, verification traceability, implementation testability를 우선한다. Harness는 제품이 아니라 C++/MSVC 기반 FEM 구조해석 솔버 개발을 통제하는 운영 인프라이다. +## 목적과 기록 범위 ---- +FESA는 유한요소법 기반 구조해석 솔버를 단계적으로 구축한다. 이 문서는 그 과정에서 +프로젝트 전반에 적용되는 결정의 이유와 트레이드오프를 관리한다. 제품 목표는 +[PRD](PRD.md), 공통 책임과 의존성은 [ARCHITECTURE](ARCHITECTURE.md)를 따른다. -### ADR-001: FESA는 구조해석 솔버 프로젝트이고 Harness는 운영 인프라로 둔다 -**결정**: 저장소의 주 목적은 유한요소법 기반 구조해석 솔버 개발이다. Harness는 승인된 Step 계획, 독립 세션 실행, PreToolUse guardrail, Stop MSVC build/test 검증을 제공하는 보조 계층으로 유지한다. +특정 요소·재료·하중·해석 절차의 정식화, 수치 기준과 구현 선택은 +`docs//ADR.md`에 기록한다. 해당 Feature의 목적·범위·acceptance 요약은 +같은 폴더의 `PRD.md`, 상세 전문 계약은 requirements·formulation·I/O 등의 소유 문서에 둔다. +공통 ADR에 기능 목록, 진행 상태와 개별 실행 결과를 누적하지 않는다. -**이유**: 기존 문서가 Harness 중심이면 agent가 solver architecture, FEM verification, Abaqus/HDF5 계약보다 운영 스크립트에 과도하게 맞춰 행동한다. +## 기존 결정의 보존과 읽는 방법 -**트레이드오프**: Harness 문서의 비중은 낮아지지만, `docs/HARNESS_WORKFLOW.md`의 실행 계약과 `.codex/hooks.json`의 검증 정책은 계속 필수 운영 규칙으로 유지한다. +개정 전 ADR-001~023의 전체 원문은 +[원문 보존본](archive/pre-generalization/ADR.md)에 있다. 아래 기존 ID 항목은 그 결정의 +공통 부분을 요약한 것이며 번호 재사용, 새 physics 승인 또는 기존 세부 계약의 폐기가 아니다. +구체적인 적용 범위는 원문의 같은 ID와 해당 Feature의 승인된 최신 계약을 함께 확인한다. -### ADR-002: C++17/MSVC/CMake/CTest를 기본 구현 환경으로 둔다 -**결정**: 기본 solver 구현과 validation은 C++17 이상, Visual Studio 17 2022 또는 -Visual Studio 18 2026 generator, x64 platform, Debug config, CMake, CTest로 수행한다. -`.harness/config.json`이 없으면 CMake가 설치된 두 승인 generator 중 하나를 자동 -선택할 수 있으며, build/test evidence에는 실제 generator와 compiler version을 기록한다. +- 과거 결정을 소급 수정하거나 모든 과거 상태를 일괄 `Accepted`로 표시하지 않는다. +- 일부만 대체된 결정은 유지되는 부분과 대체된 부분을 구분한다. +- 현재 선택된 toolchain, 외부 형식과 구현별 계약은 [AGENTS.md](../AGENTS.md) 및 승인된 + 설정·Feature 계약을 따른다. 문서 일반화만으로 이를 교체하지 않는다. +- 기존 결정 번호는 다른 의미로 재사용하지 않는다. 후속 결정은 마지막 번호 다음으로 추가한다. +- ADR-024~027은 현재 agent·skill·Feature 문서 운영 계약을 기록한다. 새 해석 기능이나 + 외부 실행 권한을 승인하는 결정이 아니다. -**이유**: FESA의 목표 환경은 Windows/MSVC 기반 C++이다. CMake/CTest는 solver source tree가 추가되거나 확장될 때 가장 일관된 build/test entry point다. VS17과 VS18을 -승인하면 현재 지원되는 MSVC 개발 환경에서 같은 C++17/x64/Debug 계약을 검증할 수 있다. +## 프로젝트 공통 결정 -**트레이드오프**: FESA solver source는 CMake/CTest를 기본으로 유지한다. Generator별 -compiler 차이는 각 build/test report에 명시하고, 두 generator 중 하나에서만 관찰되는 -실패를 다른 generator의 성공으로 숨기지 않는다. Harness 자체는 `.harness/config.json`에 -solution과 test command를 명시한 직접 MSBuild 프로젝트도 검증할 수 있지만, 이는 FESA -제품이 solution-only workflow를 지원한다는 의미가 아니다. +### ADR-001: Solver 제품과 개발 운영 인프라를 분리한다 -### ADR-003: Abaqus `.inp` full compatibility가 아니라 기능별 keyword subset을 지원한다 -**결정**: FESA parser는 Abaqus keyword/data/comment line 규칙을 따르되, 기능별로 승인된 keyword subset만 지원한다. 미지원 keyword는 명확한 diagnostic을 남긴다. +**결정**: 프로젝트의 제품은 구조해석 솔버다. Agent·skill·Harness는 소스, 문서와 +검증 evidence를 관리하는 개발 운영 영역이며 solver runtime의 의존성이 아니다. -**이유**: full Abaqus compatibility는 초기 solver 범위와 검증 비용을 과도하게 키운다. 기능별 subset은 요구조건, I/O contract, reference validation을 추적 가능하게 만든다. +**이유**: 개발 자동화의 편의가 물리적 의미, 사용자 요구와 수치 검증을 대신하지 않게 한다. -**트레이드오프**: 사용자는 기존 Abaqus input file을 그대로 사용할 수 없을 수 있다. 대신 지원 범위와 실패 원인이 명확해진다. +**트레이드오프**: Solver의 완료 evidence와 자동화 자체의 correctness를 별도로 관리해야 한다. +현재 운영 역할과 검증 범위는 ADR-024·026에서 보완한다. -### ADR-004: Domain, AnalysisModel, DofManager, AnalysisState를 분리한다 -**결정**: `Domain`은 `ElementDefinition`, `ElementProperty`, `Material`과 -`StepDefinition` 입력 모델 정의를 `std::unique_ptr`로 단독 소유하고 const access와 -stable collection index를 제공한다. `AnalysisModel`은 Domain 수명 안에서 stable index와 -const reference만 사용하는 non-owning current-step view다. `DofManager`는 equation -numbering과 constrained/free mapping을 전담하고, `AnalysisState`는 해석 중 변하는 -물리량을 소유한다. +### ADR-002: 구현·검증 환경은 명시된 프로젝트 baseline으로 관리한다 -**이유**: 모델 정의, step activation, equation system, transient/nonlinear state가 섞이면 parser, assembler, solver, result writer가 강하게 결합된다. 분리된 상태 모델은 선형 정적 해석에서 시작해 비선형, 동적, thermal coupling으로 확장하기 쉽다. +**결정**: 현재 FESA의 C++ 언어·compiler·platform·build/test baseline을 프로젝트 지침과 +설정으로 명시하고 실제 실행 환경을 evidence에 기록한다. 재사용 Harness가 다른 +backend를 처리할 수 있다는 사실을 FESA 제품 지원으로 간주하지 않는다. -**트레이드오프**: 초기 class 수가 늘어난다. V0에서는 interface를 얇게 유지하고 실제 선형 정적 frame에 필요한 state만 구현한다. +**이유**: 개발 환경의 암묵적 차이가 build와 numerical regression 판정을 흐리지 않게 한다. -### ADR-005: 공식 결과 파일은 HDF5로 하고 reference 결과는 Abaqus CSV로 둔다 -**상태**: HDF5 authoritative output 결정은 유지하며 reference bundle governance 부분은 ADR-019로 대체됨. +**트레이드오프**: 새 환경은 별도의 compatibility와 실행 evidence가 필요하다. 원문의 +기술 선택 이력은 유지하며, 현재 preset·명령 선택은 ADR-026의 구성 경계를 따른다. -**결정**: FESA solver의 authoritative result output은 `results.h5` HDF5이다. Abaqus reference results는 기능별 reference model contract가 지정한 `reference//` 아래 CSV 파일로 저장하며, verification은 FESA HDF5 rows와 Abaqus reference CSV rows를 documented IDs, components, units, coordinate system, step/frame identity, tolerance 기준으로 비교한다. 신규 reference는 canonical 파일명을 사용하고, 승인된 기존 bundle의 legacy alias는 해당 기능 계약에 정확한 경로를 기록한 경우에만 허용한다. +### ADR-003: 외부 입력은 승인된 subset의 의미로 지원한다 -**이유**: 구조해석 결과는 step/frame, field/history, node/element/integration point location, units, coordinate system, schema version을 함께 가져야 한다. HDF5는 이 계층 구조와 metadata를 안정적으로 표현한다. +**결정**: 외부 형식의 syntax와 semantic mapping을 구분하고 지원·미지원 범위를 명시한다. +미지원 모델 의미는 fail-closed로 진단하며 무해하다고 계약된 항목만 warning/no-op으로 둔다. -**트레이드오프**: reference comparison은 FESA HDF5 dataset identity와 Abaqus CSV row identity를 모두 관리해야 한다. FESA HDF5에서 추출한 deterministic CSV view는 디버깅/검토용 보조 artifact로 허용하지만, 공식 solver output이나 reference artifact로 취급하지 않는다. +**이유**: Parser가 읽을 수 있다는 사실과 물리적으로 해석할 수 있다는 사실은 다르다. -### ADR-006: 해석 알고리즘과 수치 backend는 Strategy와 Adapter 경계 뒤에 둔다 -**결정**: `Analysis`, `LinearSolver`, `TimeIntegrator`, `ConvergenceCriteria`는 Strategy로 구성하고, MKL, TBB, HDF5 API는 adapter 계층 뒤에 둔다. `Vector`는 contiguous `double`, `Matrix`는 row-major contiguous `double`을 소유하고 dense 연산은 MKL CBLAS를 사용한다. `SparseMatrix`는 이들과 상속 관계가 없는 0-based CSR 전용 타입이며, PARDISO factorization과 substitution은 `LinearSolver` 경계 뒤에서 분리한다. +**트레이드오프**: 기존 외부 모델을 수정해야 할 수 있지만 silent approximation과 과도한 +호환성 약속을 피할 수 있다. 실제 입력 형식의 선택은 기존 승인 계약을 유지한다. -**이유**: 선형 정적, 비선형 정적, 동적, frequency, heat transfer 해석은 공통 흐름을 공유하지만 알고리즘과 backend가 다르다. 외부 API를 core에 노출하면 테스트 double, backend 교체, dependency review가 어려워진다. +### ADR-004: 모델 정의, 실행 view, equation과 mutable state를 분리한다 -**트레이드오프**: 단일 기능만 구현할 때는 adapter가 다소 장황해 보일 수 있다. Row-major dense storage와 CSR sparse storage를 따로 유지해야 하지만 backend 의존성과 dense/sparse 의미가 core 모델에 섞이지 않는다. +**결정**: 모델 owner, non-owning 실행 view, DOF/equation owner와 procedure state를 +구분한다. View는 원본보다 오래 살지 않으며 승인된 procedure에 필요한 state만 둔다. -### ADR-007: Analysis base는 최소 실행 계약만 제공한다 -**결정**: `Analysis` base는 virtual `Analysis::Run(const AnalysisRequest&)`만 제공하고 -linear-static-specific protected hook을 정의하지 않는다. 승인된 선형 정적 순서인 -`parse input -> initialize Domain -> build AnalysisModel -> build DOF map/sparse pattern -> -assemble stiffness -> partition constraints -> factorize Kff -> assemble load -> form effective -RHS -> substitute -> reconstruct displacement -> recover results -> write HDF5`는 -`LinearStaticAnalysis::Run()`의 private lifecycle로 유지한다. 강성행렬 factorization은 -하중벡터 조립보다 먼저 수행하고 factorization과 substitution을 하나의 불투명한 solve -호출로 합치지 않는다. +**이유**: Parsing, model activation, numerical solve와 recovery의 수명이 섞이지 않게 한다. -**이유**: 현재 여덟 단계는 linear static equation, state와 failure taxonomy에 특화되어 -있다. 최소 base contract는 이 순서의 검증 가능성을 보존하면서 승인되지 않은 dynamic, -eigenvalue 또는 nonlinear procedure에 같은 protected hook과 사용하지 않는 state를 -강제하지 않는다. Factorization과 substitution 분리는 동일 강성행렬에 여러 RHS를 적용할 -수 있고 각 실패 단계를 구조화된 diagnostic으로 분류하게 한다. +**트레이드오프**: 명시적인 연결과 lifetime 관리가 필요하지만 모델 복사와 분산된 +equation numbering, 미래 state의 불필요한 할당을 줄인다. -**트레이드오프**: Procedure 사이의 lifecycle code는 base Template Method로 자동 재사용되지 -않는다. 두 번째 procedure가 승인되면 실제로 같은 단계만 focused collaborator로 추출하되, -linear-static hook 사이에 조건문으로 새 physics를 삽입하지 않는다. +### ADR-005: Authoritative result와 reference·debug artifact를 구분한다 -### ADR-008: Sparse assembly는 deterministic COO-to-CSR 경로로 시작한다 -**결정**: 초기 assembly는 element-local contribution을 COO triplet으로 수집한 뒤 CSR로 finalize한다. MKL PARDISO backend는 CSR input contract를 받는다. +**결정**: 공식 결과의 물리 identity와 versioned 저장 계약을 유지한다. Reference와 +debug view를 공식 결과로 대체하지 않는다. 기존 HDF5 선택은 유지하며 보조 형식의 존재가 +공식 형식 변경을 뜻하지 않는다. -**이유**: CSR은 MKL PARDISO와 잘 맞고, COO-to-CSR 경로는 구현과 테스트가 명확하다. deterministic reduction은 reference comparison의 재현성을 지킨다. +**이유**: 결과 검토 도구나 비교 편의를 위해 authoritative quantity와 파일 의미가 바뀌는 것을 막는다. -**트레이드오프**: 대규모 모델에서는 메모리와 변환 비용이 생길 수 있다. 성능 문제가 실제로 확인되면 typed batch assembly 또는 kernel 분리를 추가한다. +**트레이드오프**: 물리 identity와 저장 mapping을 함께 관리해야 한다. +원문의 reference naming 규칙은 ADR-019로 대체되었고, 파일명·경로의 후속 결정은 +보존본 ADR-023에 있다. 이를 문서 정리 과정에서 과거 값으로 되돌리지 않는다. -### ADR-009: TBB 병렬화는 element-local computation부터 적용한다 -**결정**: 첫 oneTBB 적용 지점은 element-local matrix/residual 계산이다. 전역 sparse write는 thread-local buffer 또는 deterministic reduction으로 제한한다. +### ADR-006: 알고리즘과 backend를 명시적인 interface 뒤에 둔다 -**이유**: element-local 계산은 독립성이 높고 병렬화 효과가 명확하다. 전역 sparse matrix에 직접 병렬 write하면 재현성, race, ordering 문제가 생긴다. +**결정**: 승인된 analysis와 numerical·parallel·output backend의 계약을 implementation과 +분리한다. Vendor/OS 타입과 resource lifecycle은 private adapter에 둔다. -**트레이드오프**: 초기 병렬화 범위가 제한된다. MKL 내부 thread와 TBB task arena의 oversubscription 정책을 별도로 문서화해야 한다. +**이유**: 해석의 물리 의미가 특정 library API와 오류 처리에 종속되지 않게 한다. -### ADR-010: Abaqus reference artifact는 사람이 생성하거나 명시 승인된 절차로만 갱신한다 -**상태**: Artifact read-only 및 실행 제한은 유지하며 metadata/provenance/naming 계약은 ADR-019로 대체됨. +**트레이드오프**: Adapter와 계약 테스트 비용이 생긴다. 실제 사용 사례가 없는 Strategy, +factory, 공통 base나 future capability를 미리 도입하지 않는다. -**결정**: Agent는 Abaqus, Nastran 또는 reference solver를 직접 실행하지 않는다. reference artifact 생성, 수정, 복원은 명시 승인된 phase에서만 수행한다. 모든 bundle의 provenance, generator/version, units, coordinate system, step/frame identity, schema, tolerance와 limitations는 승인된 기능별 Reference Model Contract에 기록한다. `metadata.json`은 선택 reference artifact이며, 부재만으로 bundle을 불완전하다고 판정하지 않는다. 파일이 존재하면 read-only 보조 자료로 inventory하고 계약 및 실제 artifact와 일치하는지 확인하며, 충돌은 숨기지 않고 upstream 계약 문제로 보고한다. 승인된 `cantilever-beam-b33` legacy baseline의 space-containing filename과 `README.md` N/A 예외는 유지한다. +### ADR-007: 해석 procedure가 자신의 lifecycle을 소유한다 -**이유**: reference 결과는 solver correctness의 기준이다. 생성 절차가 불명확하면 구현 결함과 reference artifact 오류를 구분할 수 없다. +**결정**: 공통 analysis 경계는 최소 실행 계약만 제공한다. 구체적인 state, assembly, +solve, 반복·종료와 recovery 순서는 procedure별 승인 계약으로 정의한다. -**트레이드오프**: Reference Model Contract가 필수 provenance의 단일 source of truth가 되어 별도 JSON 파일 없이도 bundle을 사용할 수 있다. 선택 `metadata.json`이 계약과 중복될 수 있으므로 존재 시 일관성 검사가 필요하다. Legacy filename과 `README.md` 예외는 일반 artifact 규칙을 복잡하게 하지만 exact path와 source commit을 고정하고 read-only로 취급해 감사 가능성을 유지한다. +**이유**: 서로 다른 해석의 의미를 하나의 고정된 Template Method나 조건문에 강제하지 않는다. -### ADR-011: 구형 단일 검증 진입점 계약을 폐기한다 +**트레이드오프**: Procedure 사이의 일부 코드가 중복될 수 있다. 승인된 연산 순서는 +보존하며 실제 공통 책임이 확인된 경우에만 collaborator로 분리한다. -**상태**: ADR-012로 대체됨. +### ADR-008: Local contribution과 global assembly를 분리한다 -**결정**: 삭제된 legacy validation entry point, legacy Python test discovery, 환경 변수 기반 command override를 Harness의 기본 검증 계약으로 사용하지 않는다. +**결정**: Local 계산은 명시된 ordering으로 contribution을 반환하고, 전역 조립은 지정된 +pattern·scatter와 deterministic reduction을 사용한다. -### ADR-012: Harness는 계획, 실행, Hook 검증의 세 계층으로 운영한다 +**이유**: 요소 수식과 전역 연결 오류를 분리해 검증하고 scheduling에 의존하지 않게 한다. -**결정**: 계획은 `.agents/skills/harness`, Step 실행과 Git 상태 관리는 `scripts/execute.py`, 도구 호출 전 정책과 종료 전 검증은 `.codex/hooks.json`에 연결된 `scripts/hooks/`가 담당한다. C++ production 변경은 관련 테스트 파일이 있어야 하며, 실제 RED 실패와 GREEN 성공은 같은 Step 안에서 확인한다. Stop은 `.harness/config.json` 또는 자동 감지 결과로 전체 MSVC build/test를 검증한다. Harness Python 변경은 `uv run --with pytest python -m pytest -v -rs`로 검증한다. +**트레이드오프**: Buffer와 변환 비용이 생긴다. 기존 저장·조립 방식의 교체는 별도 evidence와 +계약 검토가 필요하며 이 요약은 비결정적인 경로를 허용하지 않는다. -**이유**: 테스트 파일 존재 검사, TDD 실행 증거, 전체 build/test는 서로 다른 책임이다. 이를 분리하면 Hook이 보장하는 범위를 과장하지 않으면서 Step 종료 시 green 상태를 강제할 수 있다. +### ADR-009: 병렬화는 소유권이 명확한 독립 작업에 적용한다 -**트레이드오프**: PreToolUse만으로 구현 전 RED 실행을 증명할 수 없으므로 Implementation report에 RED/GREEN 명령과 결과를 기록해야 한다. C/C++가 없는 저장소는 Stop이 통과하므로 Harness Python 검증은 별도 acceptance command로 유지한다. +**결정**: Worker는 자신의 local output에만 기록하고 공유 결과의 reduction과 diagnostic +선택은 결정적으로 수행한다. -### ADR-013: V0 선형 정적 해석은 단일 step B33 Euler beam으로 제한한다 +**이유**: Race와 부동소수 누적 순서가 물리 검토·회귀 비교를 흔들지 않게 한다. -**결정**: 첫 end-to-end 제품 기능은 입력 파일당 하나의 `*STEP, *STATIC`을 해석하는 2절점 3D Euler–Bernoulli beam이다. Abaqus `TYPE=B33`만 이 요소로 매핑하고 B31은 `unsupported-element-formulation`으로 거부한다. `*PART/*ASSEMBLY/*INSTANCE`는 여러 identity instance와 stable source identity를 지원하지만 instance transform, nested assembly, 다중 step은 지원하지 않는다. Output-request keyword는 승인 allowlist에서 warning 후 no-op 처리하고 FESA 기본 HDF5 결과는 항상 생성한다. +**트레이드오프**: 병렬화 범위와 throughput이 제한될 수 있다. Backend thread와 task +정책은 실제 측정과 검증을 통해 정하며 승인된 ordering을 보존한다. -**이유**: B31은 transverse shear deformation을 포함하므로 Euler–Bernoulli 정식화와 의미가 다르다. 단일 step과 identity instance로 범위를 제한하면 parser wrapper, source identity, assembly, 해법 및 결과 복구를 end-to-end로 검증하면서 의미를 왜곡하지 않는다. +### ADR-010: 외부 reference는 명시적인 소유권과 권한 아래 유지한다 -**트레이드오프**: 기존 Abaqus 모델 중 B31, instance transform, 다중 step 또는 `*DLOAD`를 사용하는 입력은 거부된다. 이후 기능은 별도 요구조건과 formulation/reference gate를 거쳐 추가한다. +**결정**: 저장된 reference는 read-only다. 생성·수정·복원과 외부 solver 실행은 명시적으로 +승인된 범위에서만 다루며, 구현을 통과시키기 위해 artifact를 보정하지 않는다. -### ADR-014: B33 cantilever baseline은 component-scale 혼합 tolerance로 비교한다 +**이유**: 검증 기준과 구현 결과가 서로를 정당화하는 순환을 방지한다. -**상태**: Artifact inventory와 component mapping의 역사적 결정은 유지하며 tolerance와 -node-station projection 결정은 ADR-022로 대체됨. +**트레이드오프**: 필요한 artifact가 없으면 적절한 owner나 환경 조치가 필요하다. +원문의 추가 metadata·provenance·naming 조건은 ADR-019로 대체되었다. -**결정**: `reference/cantilever beam/`의 B33 input, displacement CSV, reaction CSV, elemental-force CSV를 V0 reference baseline으로 사용한다. Displacement와 reaction은 node identity로, section resultant는 CSV node station과 정규화된 FESA element endpoint로 비교한다. 같은 model, step/frame, quantity, component에 대해 `reference_scale = max(abs(reference rows))`를 계산하고 모든 row에 `absolute_floor + 1e-6 * reference_scale`을 적용한다. SI displacement/rotation absolute floor는 `1e-9`, force/moment floor는 `1e-3`이다. Beam stress는 출력하되 Abaqus stress reference comparison은 N/A다. +### ADR-012: 계획, 실행과 Hook 검증의 책임을 분리한다 -**이유**: 자유단 moment처럼 이론적으로 0인 값에는 행별 상대오차가 정의되지 않으며 Abaqus 결과에 작은 수치 잔차가 남을 수 있다. Component scale을 사용하면 전체 물리량 크기에 비해 작은 잔차를 허용하면서 각 row를 결정적으로 판정할 수 있다. +**결정**: Planning은 할당된 계획 파일, Executor는 실행 상태와 Git 처리, Hook은 등록된 +guardrail과 종료 검증을 담당한다. 테스트 파일 존재는 RED 관찰의 증거가 아니다. -**트레이드오프**: 큰 component scale은 zero row의 허용폭을 넓힌다. 이를 완화하기 위해 quantity와 component를 섞지 않고 Abaqus 값만 scale에 사용하며, 모든 row의 absolute/normalized error와 worst row를 보고하고 누락·추가·nonfinite row를 tolerance 전에 실패시킨다. +**이유**: 자동화 각 계층이 보장하는 범위를 명확히 하여 검증 누락과 과장된 통과 판정을 막는다. -### ADR-015: 외부 CMake target을 FESA 경계로 정규화하고 Windows runtime closure를 명시한다 +**트레이드오프**: Implementation은 실제 명령·결과를 별도 evidence로 남겨야 한다. +원문의 전체 Stop 검증 설명은 ADR-026의 조건별 검증 범위와 함께 읽고, 계획·실행의 +권한은 ADR-024의 현행 운영 계약을 따른다. -**결정**: MKL, TBB, HDF5 CONFIG package가 제공하는 target은 dependency module에서 -`Fesa::MKL`, `Fesa::TBB`, `Fesa::HDF5`로 정규화한다. Product target은 이 경계만 -`PRIVATE` link하고 public header에 vendor/Win32 type을 노출하지 않는다. Approved Windows -환경에서는 packaged shared HDF5 C target을 우선하며, CLI와 test executable 옆에 TBB, -MKL, OpenMP, Intel math runtime, HDF5를 포함한 실제 runtime DLL closure를 POST_BUILD로 -stage한다. +### ADR-015: Build dependency와 runtime closure를 명시한다 -**이유**: Package version이나 shared/static variant에 따라 imported target 이름과 숨은 -runtime dependency가 달라질 수 있다. Configure와 link 성공만 확인하면 GoogleTest -discovery 또는 배포된 CLI가 `0xc0000135`로 실패할 수 있다. FESA-owned target과 명시적 -runtime closure는 module code와 실행 환경을 package 세부사항에서 분리한다. +**결정**: 외부 dependency를 프로젝트가 소유한 target 경계로 정규화하고 public API에 +vendor type을 노출하지 않는다. 실행에 필요한 dynamic dependency도 검증 범위에 포함한다. -**트레이드오프**: Windows staging CMake가 길어지고 새 dynamic dependency마다 runtime -audit가 필요하다. 대신 개인 설치 경로를 source에 하드코딩하거나 developer `PATH`에 -의존하지 않고 build tree executable을 재현 가능하게 실행할 수 있다. +**이유**: Configure·link 성공만으로 실제 실행 환경의 완전성을 증명할 수 없다. -### ADR-016: 결정론과 failure atomicity를 correctness contract로 취급한다 +**트레이드오프**: Backend 추가마다 build 및 runtime 검토가 필요하다. 구체적인 package +선택·staging 구현은 현재 설정과 기존 승인 계약에 유지한다. -**결정**: Parallel element 계산은 index별 독립 buffer만 쓰고, COO tuple ordering과 -floating-point sum은 join 이후 고정된 serial 순서로 수행한다. Result recovery, solver -output, HDF5 writer는 candidate를 완성·검증한 뒤 caller-owned state 또는 final file에 -commit한다. HDF5는 same-directory temporary file을 checked close/reopen한 후에만 atomic -finalization한다. +### ADR-016: 결정론과 failure preservation을 correctness로 취급한다 -**이유**: Reference comparison과 물리 검토는 row identity와 작은 수치 차이에 민감하다. -Thread scheduling 또는 중간 실패가 sparse bytes, result order, 이전 output을 바꾸면 같은 -입력에 대한 evidence를 신뢰할 수 없다. +**결정**: Ordering과 reduction을 명시하고, state·result·final output은 검증된 candidate를 +통해 반영한다. 실패 시 계약된 기존 유효 상태와 최종 결과를 보존한다. -**트레이드오프**: Serial reduction과 candidate copy가 throughput과 memory를 더 사용할 수 -있다. 성능 최적화는 동일한 ordering, failure preservation, reference 결과를 증명할 때만 -대체할 수 있으며, nondeterministic global sparse write는 허용하지 않는다. +**이유**: 재현 가능한 결과와 실패 후 신뢰할 수 있는 상태가 검증의 전제이기 때문이다. -### ADR-017: Essential constraint는 stable elimination으로 적용하고 reaction은 full residual로 정의한다 +**트레이드오프**: Buffer, candidate와 검증 비용이 증가한다. 성능 개선은 같은 보존 +경계와 결과 의미를 입증해야 하며 correctness를 약화하는 예외로 처리하지 않는다. -**결정**: V0는 DofManager의 stable free/constrained order로 full stiffness를 -`Kff/Kfc/Kcf/Kcc`로 분할하고 `Kff * df = Ff - Kfc * dc`를 푼 뒤 full displacement를 -복구한다. 모든 DOF가 constrained인 모델의 `0 x 0 Kff`는 유효한 trivial factorization과 -empty substitution이다. Recovery의 full residual `r = K*d - F`를 reaction vector로 -보존하며 constrained entry는 physical reaction, free entry는 equilibrium evidence다. +### ADR-017: 제약 적용과 reaction 의미를 equation 계약으로 정의한다 -**이유**: Equation numbering과 prescribed displacement를 한 owner가 관리하면 nonzero -constraint, no/all/mixed constraint가 같은 수식과 stable order를 사용한다. Full residual은 -element end action을 별도로 합산하는 경로보다 조립·하중·해법 전체의 equilibrium을 직접 -검증한다. +**결정**: DOF mapping, 제약 적용, 해 복구, residual과 reaction의 의미를 명시적인 +수학 계약으로 묶는다. Node/Element에 equation 번호를 분산 저장하지 않는다. -**트레이드오프**: Full K와 네 partition을 유지하는 메모리 비용이 있고 reaction vector의 -free entry가 정확한 0이라고 가정할 수 없다. V0에는 penalty, MPC, Lagrange multiplier를 -혼합하지 않으며, 추가 constraint policy는 별도 formulation과 equation/output 계약을 -요구한다. +**이유**: 서로 다른 space나 enforcement 방식의 값을 같은 물리량으로 오인하지 않게 한다. -### ADR-018: Kernel 존재와 제품 노출을 분리하고 계층형 검증을 요구한다 +**트레이드오프**: 새 constraint 방식은 equation과 output의 영향을 함께 검토해야 한다. +원문에 있는 기존 elimination·full-residual 결정은 해당 기능 범위에서 계속 유지되며, +이 공통 요약이 다른 방식을 기존 구현에 적용할 권한을 주지 않는다. -**결정**: Local element/load/recovery kernel이 존재해도 syntax, semantic Domain mapping, -analysis orchestration, mandatory HDF5와 diagnostic 계약이 연결되지 않으면 CLI 지원으로 -간주하지 않는다. 검증은 unit, integration, reference comparison, physics sanity를 서로 -대체할 수 없는 계층으로 유지한다. Reference가 가능하지 않은 quantity는 명시적 N/A와 -analytical/schema/physics 대체 evidence를 요구한다. +### ADR-018: Kernel 존재와 제품 노출을 구분하고 계층형 검증을 요구한다 -**이유**: Unit test는 수식과 validation을, integration test는 lifetime과 호출 순서를, -reference comparison은 외부 solver와의 수치 일치를, physics sanity는 공통-mode 오류와 -model adequacy를 검출한다. 한 계층의 성공만으로 parser exposure 또는 물리 correctness를 -추론하면 지원 범위와 검증 강도가 과장된다. +**결정**: 입력 의미, 내부 해석, 결과·진단과 필수 검증이 연결되어야 제품 기능으로 다룬다. +Local test, integration, reference 비교와 독립 물리 검토는 서로를 대체하지 않는다. -**트레이드오프**: 작은 기능도 여러 계약과 evidence를 함께 준비해야 하므로 개발 속도가 -느려진다. 대신 `*DLOAD`처럼 kernel은 있지만 입력에 노출되지 않은 기능, stress처럼 -mandatory output이지만 Abaqus reference가 N/A인 기능을 정확하게 표현할 수 있다. +**이유**: 부분 구현이나 외부 수치 일치만으로 제품 지원과 correctness가 과장되는 것을 막는다. -### ADR-019: Abaqus는 입력 형식과 외부 수치 reference이며 FESA 내부 동작 계약이 아니다 - -**결정**: FESA는 Abaqus와 독립적인 솔버다. 기능별 승인 `.inp` subset을 입력으로 -사용하고, 기능이 blocking으로 선언한 FESA HDF5 quantity만 기존 Abaqus CSV와 승인 -tolerance로 비교한다. Abaqus 요소 정식화, 적분, stabilization, 내부 상태와 결과 생성 -절차를 재현하거나 동등하게 구현하지 않는다. Exact numerical equality는 허용되지만 -내부 동작 동등성의 evidence가 아니다. - -Reference case readiness에는 선언된 `.inp`, 실제 비교에 필요한 CSV, deterministic -source-ID/component matching과 tolerance만 필요하다. 기존 path와 filename을 그대로 -사용하며 canonical naming, legacy-alias 승인, bundle `README.md`, `metadata.json`, Abaqus -version/provenance, 중복 units/coordinates/model/step/frame/material/section 정보와 CSV schema -version은 요구하지 않는다. Reference artifact는 계속 read-only이며 누락, 추가, 중복, -nonfinite required row는 tolerance 전에 실패한다. - -**이유**: Reference comparison의 목적은 FESA의 독립 정식화가 승인된 observable quantity를 -충분히 가깝게 계산하는지 판정하는 것이다. 수치 비교에 사용되지 않는 artifact -거버넌스가 formulation review나 implementation planning을 차단하면 제품 검증보다 문서 -형식 준수가 우선된다. 같은 정보는 `.inp`, CSV header와 feature contract에서 직접 얻을 -수 있다. - -**트레이드오프**: Reference 생성 환경을 사후에 완전히 재구성하는 감사 기능은 줄어든다. -대신 비교 대상과 source-row/component mapping, tolerance, artifact immutability는 유지해 -false match와 결과 보정을 방지한다. 더 강한 provenance가 필요한 기능은 해당 요구조건이 -명시적으로 추가할 수 있으나 프로젝트 기본 gate로 자동 승격하지 않는다. - -### ADR-020: MITC4 displacement reference는 고정 절대오차로 판정한다 - -**상태**: S4 artifact 선택과 U blocking/UR warning-only 분류는 유지하며 고정 절대오차 -결정은 ADR-022로 대체됨. - -**결정**: Full-integration FESA-MITC4의 sole S4 reference comparison은 matched global -`U1/U2/U3` row에 고정 절대오차 `1.0e-5`를 적용해 pass/fail을 판정한다. -`UR1/UR2/UR3`도 고정 절대오차 `1.0e-5`로 비교하지만 초과는 deterministic warning만 -생성한다. Reference scale은 보고용 통계로 남을 수 있으나 MITC4 row tolerance 또는 -normalized error의 분모를 결정하지 않는다. ADR-014의 B33 component-scale 혼합 -tolerance는 변경하지 않는다. - -**이유**: Abaqus S4는 FESA-MITC4와 동일한 요소 정식화가 아니므로 B33에서 승인한 매우 -엄격한 component-scale tolerance를 그대로 재사용하는 것은 독립 솔버의 observable -displacement 검증 목적에 맞지 않는다. 고정 절대오차는 현재 사용자 단위계에서 승인된 -차이를 명시적으로 제한하면서 zero clamp나 row omission 없이 모든 displacement row를 -동일하게 판정한다. - -**트레이드오프**: Model scale이 크게 달라지면 고정 절대오차의 상대적 엄격도가 달라질 수 -있다. 따라서 이 값은 현재 승인된 MITC4 S4 case의 기능 완료 기준이며 개발 완료 후 -별도 reference-verification evidence와 함께 재점검한다. - -### ADR-021: Semantic definition과 runtime solver contract를 분리한다 - -**결정**: Domain-owned semantic definition과 analysis-time numerical object를 다음 -dependency 방향으로 분리한다. - -```text -Domain owns ElementDefinition / ElementProperty / Material / StepDefinition -AnalysisModel is a non-owning stable-index view into Domain -ElementFactory creates runtime Element candidates from compatible definitions -DofManager -> ElementDofLayout -SparseAssembler -> ElementStiffnessContribution -ResultRecovery -> ElementResultBundle -LoadAssembler -> ordered LoadContribution -EssentialConstraintPolicy -> ConstraintDefinition -Analysis <- LinearStaticAnalysis -``` - -`Domain`의 polymorphic semantic collection은 `std::unique_ptr` 단독 ownership과 stable -vector position을 사용한다. `ElementDefinition`은 source identity, connectivity와 -property/material identity를 제공하고 runtime `Element`는 DOF layout, stiffness와 result -recovery를 제공한다. `ElementFactory`가 definition/property/material compatibility를 -중앙에서 검사하며 unknown 또는 incompatible 조합은 fail-closed diagnostic으로 거부한다. -Consumer는 B33/MITC4 concrete type branch를 분산시키지 않고 runtime contract를 사용한다. -Linear-static candidate가 runtime `Element`를 `std::unique_ptr`로 소유하고 consumer는 그 -수명에 한정된 non-owning view만 사용한다. - -`Material` base에는 identity, source location과 lifetime 이외의 future capability를 -추가하지 않는다. 현재 isotropic linear elasticity가 실제로 요구하는 data는 concrete -material에 둔다. Density, anisotropy, plastic state, temperature와 rate dependency는 -optional field 또는 no-op virtual method로 미리 할당하지 않는다. `ElementProperty`도 -현재 beam/shell 의미를 각 concrete type에 둔다. - -`Load`는 ordered `LoadContribution`을 생성하고 global full-DOF accumulation은 -`LoadAssembler`가 단독 소유한다. `BoundaryCondition`은 `ConstraintDefinition`을 생성하고 -`EssentialConstraintPolicy`가 prescribed-displacement elimination과 reconstruction을 -소유한다. Distributed/body load와 MPC/penalty/Lagrange-multiplier enforcement는 이번 -결정으로 구현된 기능이 아니다. - -Abaqus Domain mapper, result recovery와 HDF5 writer는 기존 public facade를 유지하면서 -각각 topology/material-property/step-final-assembly, equilibrium/beam/shell/atomic-state, -RAII/model-result-dataset/self-check/atomic-finalization 책임으로 private implementation을 -나눈다. - -**이유**: Semantic identity와 runtime kernel을 같은 concrete record에 두면 DofManager, -SparseAssembler, ResultRecovery, parser와 output이 B33/MITC4 storage를 함께 알아야 한다. -Definition/factory/runtime contract와 contribution/policy 경계를 분리하면 stable source -identity와 deterministic reduction owner를 유지하면서 실제 두 element 구현을 공통 -consumer로 연결할 수 있다. Focused facade 분할은 외부 계약을 바꾸지 않고 큰 translation -unit의 서로 다른 failure-atomicity 책임을 검토 가능하게 한다. - -**트레이드오프**: Base object, factory와 contribution record가 늘고 checked compatibility에 -한 단계의 indirection이 생긴다. 대신 `std::shared_ptr`, speculative `Clone()`, global -registry와 future-only material/analysis capability는 도입하지 않는다. B33/MITC4의 승인된 -formulation, 연산·reduction 순서, sign, units, coordinates와 result identity가 이 -리팩터링보다 우선하며 HDF5 schema, reference artifact와 ADR-014/ADR-020 tolerance는 -변경하지 않는다. MITC3, solid, dynamic과 plastic behavior는 별도 feature gate 전까지 -구현된 것으로 간주하지 않는다. - -### ADR-022: 외부 reference comparison은 공통 family-scale 정책을 사용한다 - -운영 상수, 검증 순서, report schema와 변경 관리는 `docs/TOLERANCE.md`를 따른다. - -**결정**: B33, MITC4와 이후 기능의 외부 reference comparison은 동일한 무차원 수치 -정책을 사용한다. 같은 model/case, step/frame, logical quantity, unit dimension, coordinate -system과 blocking behavior의 component family에 대해 reference-only scale -`S=max(abs(reference))`를 계산한다. `abs(reference)<=0.01*S`인 행은 -`abs(fesa-reference)<=0.01*S`, 나머지 행은 `abs(fesa-reference)/abs(reference)<=0.05`를 -만족해야 한다. 모든 행과 함께 family scale-relative RMS `RMS(error)/S<=0.01`도 -통과해야 한다. 독립 absolute-error gate는 두지 않으며 `S=0`이면 모든 FESA 값도 exact -zero일 때만 통과한다. - -B33 family는 translation, rotation, reaction force, reaction moment, section force와 -section moment로 구분한다. 2026-08-18에 다시 생성된 elemental-force CSV의 -`Element Label`과 `Node Label`을 사용해 `(instance, element, endpoint node, component)`를 -HDF5 endpoint에 직접 대응하고 node-station collapse나 평균을 사용하지 않는다. MITC4는 -translation `U1/U2/U3` family를 blocking으로, rotation `UR1/UR2/UR3` family를 -warning-only로 유지한다. - -**이유**: Feature별 absolute floor와 fixed absolute tolerance는 모델 단위와 크기에 따라 -서로 다른 엄격도를 만들었다. 개별 component scale은 물리적으로 zero-like인 MITC4 -`U1/U2` residue를 자체 scale로 만들어 잘못 실패시킨다. 동일 차원의 family scale, -near-zero 대체 분기와 relative RMS를 결합하면 zero row의 불안정한 raw relative error를 -피하면서 모든 행과 전체 오차 수준을 함께 검사할 수 있다. B33의 element-endpoint identity는 -reference와 HDF5가 제공하는 실제 source identity를 보존한다. - -**트레이드오프**: Family 최대값이 작은 component의 허용폭을 결정하므로 행별 relative -gate와 family RMS gate를 모두 유지해야 한다. Reference family 전체가 zero이면 별도 -절대 scale이 없으므로 exact-zero만 허용하는 엄격한 fail-closed 정책이 된다. 기존 B33과 -MITC4 verification report 및 comparator schema는 새 정책에 맞춰 다시 생성해야 하지만 -reference artifact, HDF5 schema, element formulation과 blocking/warning 분류는 변경하지 -않는다. - -### ADR-023: Nonlinear Static은 prepared dispatch와 accepted-frame evidence로 분리한다 - -**결정**: CLI는 하나의 `.inp` operand만 받고 input lexical sibling -`_results.h5`와 `_log.txt`를 유도한다. `--output`은 지원하지 않는다. -`PrepareAnalysisInput()`이 syntax/semantic map을 한 번만 수행하고, prepared Domain의 -procedure identity가 linear static 또는 separate nonlinear full-Newton procedure를 선택한다. -Nonlinear procedure는 trial/committed state, unsymmetric tangent solver와 iteration log를 -독립적으로 소유하며 log close 뒤에만 schema-v1 HDF5 writer가 accepted frames를 atomic publish한다. - -schema-v1 `/global/equilibrium`의 six values는 global origin `[0,0,0]`에서 current `X+u` -force arm과 spatial physical nodal moments를 쓴 applied-plus-constrained-reaction balance -error다. Accepted free generalized residual과 constrained-reaction resultant는 distinct -projections이며 result dataset을 서로 대체하지 않는다. Nonlinear reference portfolio는 fine -primary `shellNL-meshfine`, coarse companion `shellNL`, linear control `shell`을 분리한다. -External U is blocking, UR is warning-only, local RF/RM is review-only, and RM3/stress is excluded; -the factor-one internal global force/moment balance and accepted free residual remain blocking. - -**이유**: Input parse, linear lifecycle, nonlinear trial state와 writer finalization을 한 -procedure에 섞으면 output lifetime과 failure atomicity를 검증할 수 없다. Applied-plus-reaction -balance, constrained reaction과 free residual을 구분하면 nonzero prescribed translation/rotation -case에서도 force arm, mask와 rotation-coordinate conversion의 의미가 보존된다. - -**트레이드오프**: Nonlinear path는 linear procedure보다 state/backend/writer 수가 늘고 fine -reference run 시간이 길다. 대신 HDF5 authoritative output, immutable CSV artifact와 external -solver internal-algorithm non-equivalence 원칙을 유지하며 local support-force distribution을 -blocking tolerance로 위장하지 않는다. +**트레이드오프**: 작은 기능도 여러 종류의 evidence가 필요하다. Reference N/A는 이유와 +승인된 대체 근거를 기록하며, 모든 기능에 동일한 benchmark 목록을 강제하지 않는다. + +### ADR-019: 외부 solver의 관찰 가능한 의미와 내부 동등성을 구분한다 + +**결정**: 외부 제품의 공개 입력 의미와 승인된 출력량은 reference로 사용할 수 있지만 +비공개 알고리즘·상태·결과 생성 절차를 추정하거나 동등성을 주장하지 않는다. +수치적 exact equality는 허용되지만 내부 동등성의 증거가 아니다. + +필요한 source identity·component와 비교 artifact를 사용하며 기존 path와 값을 보존한다. +Canonical naming이나 중복 metadata·provenance를 기본 readiness gate로 추가하지 않는다. +이는 ADR-005·010의 관련 부수 조건을 대체하며 artifact immutability는 유지한다. + +**이유**: 물리·수치 검증보다 외부 제품의 비공개 동작 추정이나 문서 형식 맞추기가 앞서지 않게 한다. + +**트레이드오프**: 더 강한 provenance가 필요한 경우 별도 명시적 요구가 필요하다. +기본 비교에서도 누락·추가·중복·nonfinite 값과 잘못된 identity는 거부한다. + +### ADR-021: Semantic definition과 runtime numerical contract를 분리한다 + +**결정**: 모델 정의는 identity와 입력 의미를 소유하고, runtime numerical object는 +승인된 evaluation·contribution·recovery 계약을 제공한다. 생성 경계가 호환성을 검사한다. + +**이유**: Parser와 global consumer가 모든 concrete element/material storage를 알 필요가 +없도록 하면서 source identity와 명시적 소유권을 유지한다. + +**트레이드오프**: Indirection과 compatibility 검증이 필요하다. Speculative cloning, +global registry와 future-only capability를 도입하지 않는다. 원문 리팩터링의 구현별 +설명은 보존본에 있으며 당시 tolerance 참조는 후속 ADR-022와 함께 읽는다. + +### ADR-022: 외부 reference comparison에 승인된 공통 정책을 적용한다 + +**결정**: 외부 reference의 family, scale, near-zero·row·RMS·zero-scale 판정과 보고·변경 +관리는 [TOLERANCE](TOLERANCE.md)를 따른다. 기능별 비교량과 identity는 해당 Numerical +Review 및 I/O 계약으로 정의한다. 이 문서는 수치 상수와 판정식을 중복 유지하지 않는다. + +**이유**: 기능마다 임의의 scale이나 오차 기준을 적용하거나 구현 결과에 맞춰 기준을 +완화하는 일을 방지한다. + +**트레이드오프**: 변경 시 관련 comparator와 evidence를 함께 재검증해야 한다. +ADR-014·020의 과거 수치 정책을 대체하지만 artifact 선택·identity·판정 영향 중 유지되는 +부분은 원문과 후속 계약을 확인한다. 이번 일반화는 tolerance 변경이 아니다. + +## 기능별·대체된 결정의 이관 기록 + +다음 ID는 프로젝트 공통 결정으로 다시 정의하지 않는다. 원문은 +[보존본 ADR](archive/pre-generalization/ADR.md)의 같은 ID에 있으며, 향후 해당 +`docs//ADR.md`를 작성할 때 원래 ID·범위·revision과 대체 관계를 연결한다. +아직 작성하지 않은 Feature ADR을 존재하거나 승인된 문서처럼 링크하지 않는다. + +| 기존 ID | 보존할 범위와 상태 | 이관 또는 조회 대상 | +| --- | --- | --- | +| ADR-011 | 구형 검증 진입점 폐기 이력; ADR-012로 대체됨 | 프로젝트 운영 이력 | +| ADR-013 | 최초 end-to-end 기능의 입력·해석 범위 결정 | `linear-static-3d-euler-beam` Feature ADR | +| ADR-014 | 최초 reference baseline 및 비교 계약 이력; tolerance와 projection은 ADR-022로 대체됨 | `linear-static-3d-euler-beam` Feature ADR | +| ADR-020 | 요소 확장의 reference 선택·판정 구분 이력; 수치 정책은 ADR-022로 대체됨 | `linear-static-mitc4-shell` Feature ADR | +| ADR-023 | 별도 procedure, accepted-frame 결과와 CLI 경로 결정 | `nonlinear-static-newton-raphson` Feature ADR; 공통 외부 경로의 후속 결정 근거로도 보존 | + +원문의 결정 당시 상태를 현재 release verdict로 해석하지 않는다. 후속 Feature가 범위를 +확장했거나 evidence가 stale인 경우 해당 승인 계약과 workflow 상태를 따로 확인한다. + +## 공통 개발 운영 결정 + +### ADR-024: Coordinator가 evidence 기반 workflow와 유한 correction을 소유한다 + +**상태**: 현재 [agent 운영 계약](SOLVER_AGENT_DESIGN.md)의 기록. +ADR-012의 계획·실행·검증 계층을 보완하며 specialist의 계약 소유권을 변경하지 않는다. + +**결정**: Coordinator만 상태를 전이하고 specialist는 할당된 전문 판단과 산출물을 +반환한다. 승인된 scope 안의 일반 작업에는 반복 승인 gate를 추가하지 않고 critical +decision만 사용자에게 요청한다. 계획 요청 자체는 실행 권한이 아니다. + +동일 normalized `retry_key`의 correction은 한 번으로 제한하고 이후 Implementation +rerun을 한 번 수행한다. Executor transport attempt와 이를 혼용하지 않는다. +필수 evidence가 누락·실패·stale이면 통과시키지 않는다. + +**이유**: 일상적인 handoff의 승인 병목을 줄이면서 제품 의미와 외부 권한을 사람이 +통제하고, 같은 실패를 무한 반복하지 않게 한다. + +**트레이드오프**: Owner, revision, evidence와 실패 signature를 명확히 기록해야 한다. +Release readiness가 외부 게시나 배포 권한을 주지는 않는다. + +### ADR-025: 프로젝트 기준과 Feature 문서의 원본·소유권을 분리한다 + +**상태**: 현재 [Feature 문서 관리](FEATURE_DOCUMENTATION.md) 계약의 기록. + +**결정**: 프로젝트 PRD·ARCHITECTURE·ADR에는 공통 기준만 둔다. Coordinator는 +Feature의 `FEATURED_AGENTS.md`, `PRD.md`, `ADR.md`, `coordination.md`와 문서 +목록·링크를 관리하고 specialist는 자신의 전문 본문과 상세 문서를 소유한다. +상태 원본은 `coordination.md`, 상세 요구사항 원본은 `requirements.md`다. + +`FEATURED_AGENTS.md`는 자동 로드되는 프로젝트 지침이 아니므로 dispatch와 Harness +step의 입력에 명시한다. 기본·상세 문서는 같은 Feature/revision과 evidence로 연결한다. +일반 Feature 작업에서 프로젝트 기준을 편집하지 않으며 공통 기준 변경은 별도 요청으로 다룬다. + +**이유**: 기능 진행 정보가 공통 지침에 누적되거나 요약과 전문 계약이 서로 다른 원본이 +되는 것을 방지한다. + +**트레이드오프**: 문서 목록과 상대 링크를 관리해야 한다. 대신 원문 복제를 줄이고 +upstream 변경의 영향과 stale evidence를 추적할 수 있다. + +### ADR-026: Harness의 구성, Git 소유권과 검증 범위를 신뢰 경계로 관리한다 + +**상태**: 현재 [Harness skill](../.agents/skills/harness/SKILL.md)과 +[프로젝트 지침](../AGENTS.md)의 실행 계약 기록. ADR-002의 구체적 실행 구성과 +ADR-012의 운영 설명을 보완한다. + +**결정**: 명령은 승인된 계획과 프로젝트 설정·preset·실제 metadata에 대조해 선택한다. +Config와 preset은 실행 가능한 입력이며 Hook이나 `shell=False`만으로 side effect가 +격리되었다고 보지 않는다. + +범위 밖 사용자 변경은 보존해야 하며, Executor는 branch·step 선택·timestamp·path-scoped +commit을 소유한다. 이 결정 당시 구현은 시작 전 clean 검사를 수행하고 비-phase 변경과 +phase metadata를 분리했지만, Step의 소스·테스트 소유 경로를 커밋 시점에 검증하지 않았다. +해당 한계와 실패 분류·기록 보존의 후속 보완은 ADR-027을 따른다. +실행 중 동시 편집을 피하고 범위 밖 변경 발견 시 완료 대신 Coordinator로 반환한다. +Transport attempt는 최대 2회이며 구현이나 상위 계약 실패를 +transport 문제로 반복하지 않는다. 권한 확대·원격·파괴 작업에는 명시적 승인이 필요하다. + +PreToolUse의 파일 존재 검사는 RED evidence가 아니다. Stop의 문서 전용 생략은 +유효한 동일 session/turn의 기록된 편집 대상이 모두 저장소 내부 Markdown인 조건에 +한정된다. 기록 누락·손상이나 비문서 기록이면 정상 검증 경로를 따르며, shell 쓰기의 +완전한 추적을 보장하지 않는다. 생략·미실행을 build/test 통과로 표시하지 않는다. + +**이유**: 자동화가 관련 없는 변경을 commit하거나 검증 범위·권한을 과장하는 것을 방지한다. + +**트레이드오프**: 실행 전 상태 검사와 명시적인 evidence가 필요하다. 문서 작업의 정적 +검토와 production 변경의 필수 실행 검증을 구분하며, 오래된 운영 문서가 현재 계약을 +덮어쓰지 않도록 실제 source와 대조한다. + +### ADR-027: Harness의 Step 결과와 커밋 범위를 명시적 계약으로 검증한다 + +**상태**: 사용자 승인된 Harness 보완 계획(실패 상태 보존, 파일 소유권 검증, 환경 오류 +분류)에 따른 결정. ADR-026의 실행 경계를 보완하며 solver·Hook·sandbox 권한을 변경하지 않는다. + +**결정**: 프로세스 종료 코드와 Step 보고를 함께 판정한다. 명시적 `error/blocked`는 +nonzero에서도 사유를 보존하고 자동 재시도하지 않는다. 인식된 sandbox helper 환경 오류는 +종료 코드 0에서도 차단한다. 실제 wrapper timeout으로 결과가 미보고된 경우만 최초 포함 +최대 두 번 시도하며, 단순 종료 코드 124나 미분류 nonzero를 transport로 추정하지 않는다. +Executor 소유 `attempt_history`는 원래 보고·프로세스 진단·판정을 재시도와 재개 때 보존한다. + +Planning은 Step마다 정확한 상대 파일 목록 `owned_paths`를 작성한다. Executor는 신뢰된 +목록으로 매 시도 후·stage 전·commit 전의 변경과 staged 경로를 검증하고 rename 양쪽 +경로를 확인한다. 범위 위반 시 사용자 변경을 보존하고 재시도·커밋·다음 Step을 중단한다. +기존 완료 이력은 필드 없이 읽되 pending Step은 실행 전에 승인 목록을 보완해야 한다. +구체적인 schema와 복구 절차는 [Harness skill](../.agents/skills/harness/SKILL.md)과 +[동작 과정](HARNESS_WORKFLOW.md)을 따른다. + +**이유**: 실패를 재시도로 덮어쓰거나 실행 중 나타난 미승인 파일을 자동 커밋하는 일을 막는다. + +**트레이드오프**: 실행할 기존 계획의 파일 목록을 준비해야 하고 실패 이력이 누적된다. +경로 검사는 filesystem sandbox나 동시 writer에 대한 원자적 격리가 아니다. 구현·metadata +커밋도 단일 transaction이 아니므로 뒤쪽 커밋 실패 시 앞선 커밋을 보존하고 오류로 반환한다. + +## 후속 ADR 작성 기준 + +프로젝트 전체의 dependency 방향, 공통 identity·수명·결과 의미, 기술 baseline, +검증 정책 또는 agent 권한이 바뀌면 공통 ADR을 추가한다. 하나의 구현 항목에만 +적용되는 선택은 Feature ADR에 기록한다. + +각 결정에는 ID, 적용 범위, 상태, 결정, 이유, 트레이드오프, 근거와 영향받는 계약을 둔다. +기존 결정을 변경하면 명시적인 대체·보완 관계와 승인 근거를 남기며 과거 원문과 실패 +evidence를 소급 수정하지 않는다. Feature 기록의 형식과 owner는 +[FEATURE_DOCUMENTATION](FEATURE_DOCUMENTATION.md)을 따른다. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1a9e02a..4c04b21 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,506 +1,263 @@ -# 아키텍처 +# FESA 구조해석 솔버 아키텍처 -## 목표 -FESA의 아키텍처 목표는 Abaqus `.inp` subset을 내부 semantic model로 변환하고, 유한요소 equation system을 구성해 구조해석 결과를 HDF5로 저장하며, reference comparison과 physics sanity가 가능한 C++17/MSVC 솔버 구조를 제공하는 것이다. +## 목적과 적용 범위 -FESA의 element formulation과 numerical path는 Abaqus와 독립적이다. Abaqus `.inp`는 -승인된 입력 형식이고 Abaqus CSV는 기능별 blocking quantity의 외부 수치 reference다. -Abaqus 내부 적분, stabilization, state 또는 recovery 동작은 FESA architecture contract가 -아니다. +FESA는 유한요소법 기반 구조해석 솔버를 단계적으로 구축한다. 이 문서는 +[PRD](PRD.md)의 제품 목표를 공통 책임, 의존성, 데이터와 수명 경계로 연결한다. +특정 요소·재료·하중의 정식화, 해석 알고리즘, 코드 목록 또는 구현 완료 현황은 다루지 않는다. -핵심 품질 속성: -- FEM formulation traceability -- explicit I/O contracts -- sparse linear algebra backend isolation -- deterministic verification -- incremental feature addition -- Harness 기반 TDD +구체적인 API, 수식, 실행 순서와 결과 schema는 `docs//`의 승인된 +정식화·I/O·구현 계약에서 정의한다. 아래의 책임 모델은 아직 없는 클래스, 공통 base, +state field 또는 디렉토리를 미리 만들라는 지시가 아니다. 기존 구현의 승인된 의미를 +일반적인 설명에 맞추어 변경하지 않는다. -현재 승인된 V0 end-to-end 기능은 `linear-static-3d-euler-beam`이다. 상세 계약의 -source of truth는 -`docs/superpowers/specs/2026-08-08-linear-static-3d-euler-beam-design.md`이며, 이 문서는 -그 계약을 전체 solver architecture의 모듈과 데이터 흐름에 배치한다. 아래에 나타난 -비선형, 동적, thermal 및 다른 element 계층은 장기 확장 경계이며 V0 지원 범위가 아니다. +설계 결정과 변경 근거는 [ADR](ADR.md), 코드 규약은 [CODINGSTYLE](CODINGSTYLE.md)을 +따른다. 현재 프로젝트의 기술 선택은 [AGENTS.md](../AGENTS.md)와 실행 가능한 +build/configuration에서 확인한다. -## 현재 구현된 디렉토리 구조 -```text -include/fesa/ # backend-neutral public C++ API -src/ - fesa/ - analysis/ # Analysis lifecycle, V0 LinearStaticAnalysis - app/ # CLI application and main - assembly/ # deterministic stiffness/load assembly, ParallelFor adapter - constraints/ # essential-constraint elimination and reconstruction - core/ # source identity, status, diagnostics - elements/ # current B33/MITC4 kernels and recovery - fem/ # DOF/equation numbering and sparse pattern - io/ - abaqus/ # .inp syntax reader and semantic Domain mapper - hdf5/ # private HDF5 writer and atomic finalization - math/ # owning Vector, row-major Matrix, 0-based CSR SparseMatrix - model/ # concrete V0 semantic records and immutable Domain - results/ # recovery records, full residual, ResultsWriter boundary - solvers/ - linear/ # LinearSolver interface and MKL PARDISO adapter -tests/ - unit/ # local contracts and failure paths - integration/ # analysis orchestration and CLI contracts - reference/ # HDF5-to-Abaqus projection and comparison -reference/ - / # declared read-only Abaqus input/required CSV pair -.agents/ - skills/ # Harness and review skills -.codex/ - hooks.json # PreToolUse/Stop hook registration - agents/ # FESA workflow custom agents - skills/ # FESA solver workflow skills -docs/ # Product, architecture, ADR, workflow artifacts -scripts/ - execute.py # Phase step executor - hooks/ # PreToolUse/Stop hook implementations - msvc_harness/ # MSVC project discovery and validation adapters -phases/ # Optional generated phase plans -``` +## 아키텍처 원칙 -`materials/`, `properties/`, `loads/`와 checked `ElementFactory`는 승인된 C++ modular -refactoring의 target boundary이며 후속 implementation Step에서 추가한다. 이 -documentation-only Step 시점에는 현재 구현 디렉토리로 표시하지 않는다. Density, -plasticity, anisotropy, nonlinear/dynamic analysis, MPC/penalty, history output과 production -validation module은 계속 장기 확장 경계이며 별도 승인 기능이 필요하다. +- 물리·수치 의미는 외부 파일 형식과 vendor API보다 독립적인 계약으로 유지한다. +- 모델 정의, 현재 실행 view, equation 구성, mutable state와 결과의 소유권을 구분한다. +- Local physics 계산과 global topology, assembly, constraint, solve 책임을 분리한다. +- Identity, units, coordinates, ordering과 failure 의미를 경계마다 명시한다. +- Candidate를 완성·검증한 뒤 계약된 state나 authoritative output에 반영한다. +- 병렬화와 최적화에서도 결정적인 ordering과 failure preservation을 유지한다. +- 실제 승인된 사용 사례에 필요한 최소 확장만 도입한다. -## Harness Execution Layer - -Harness는 solver core와 분리된 세 계층의 개발 운영 인프라다. - -- 계획 계층: `.agents/skills/harness`가 사용자 승인 전 Step 초안을 만들고, 승인 후 `phases/` 파일을 생성한다. -- 실행 계층: `scripts/execute.py`가 `feat-` 브랜치에서 Step마다 독립 Codex 세션을 실행하고 상태와 커밋을 관리한다. -- 검증 계층: `.codex/hooks.json`이 `scripts/hooks/pre_tool_use.py`와 `scripts/hooks/stop_validation.py`를 연결한다. Stop 검증은 `scripts/msvc_harness/`를 통해 MSVC build와 test를 실행한다. - -Runner는 `git add -A`를 사용하므로 clean worktree 또는 별도 Git worktree가 실행 전제다. 전체 동작 계약은 `docs/HARNESS_WORKFLOW.md`, 설치와 `.harness/config.json` 설정은 `docs/HARNESS.md`를 source of truth로 삼는다. - -## CMake target과 dependency graph - -Root CMake project는 C++17, extension off, CMake 3.25 이상을 요구하고 -`cmake/FesaDependencies.cmake`, `src/fesa`, `tests`를 차례로 구성한다. +## Solver runtime과 개발 운영 영역 ```text -MKL CONFIG package ─> Fesa::MKL ─┐ -TBB CONFIG package ─> Fesa::TBB ─┼─> fesa_solver (STATIC) ─> fesa_cli -HDF5 CONFIG package ─> Fesa::HDF5 ─┘ │ - ├─> fesa_unit_tests -approved local GoogleTest source ─> GTest targets ├─> fesa_integration_tests - └─> fesa_reference_tests +Solver runtime +입력 adapter → semantic model → analysis procedure → recovery/results → 출력 adapter + │ + ├─ DOF / equation / sparse pattern + ├─ element / material / load contribution + ├─ assembly / constraint policy + └─ numerical backend + +개발 운영 영역 +사용자 요구 → Coordinator → 담당 specialist → 산출물과 evidence → gate 판정 + │ + └─ 할당된 계획 / Harness / 코드·테스트·문서 변경 ``` -- `FESA_GTEST_SOURCE_DIR`는 네트워크 fetch 대신 승인된 local GoogleTest source checkout을 가리키는 필수 cache path다. -- MKL, TBB, HDF5는 CONFIG package로 탐지한다. Package search가 설치를 찾지 못하면 `MKL_DIR`, `TBB_DIR`, `HDF5_DIR`를 configure 때 지정한다. -- Package별 imported target 이름은 `Fesa::MKL`, `Fesa::TBB`, `Fesa::HDF5`로 정규화한다. Product module은 vendor target 이름을 직접 선택하지 않는다. -- HDF5 package가 shared와 static C target을 모두 제공하면 approved Windows environment에서는 shared target을 우선한다. Static archive의 숨은 compiler-runtime 요구가 link interface 밖으로 새는 것을 피하기 위한 결정이다. -- `fesa_solver`는 외부 dependency를 `PRIVATE`으로 link하고 `/W4 /WX`를 사용한다. 따라서 public header는 MKL/TBB/HDF5/Win32 type을 포함하지 않아야 한다. -- Test executable은 unit, integration, reference 경계를 분리하며 `fesa_tests` target은 세 executable을 build하는 aggregate target이다. Reference target에만 source/build root compile definition을 제공한다. +첫 흐름은 해석 데이터의 전달을, 두 번째는 개발 작업과 검증의 흐름을 나타낸다. +Agent, prompt, phase 파일과 Harness는 solver runtime API나 물리 모델의 일부가 아니다. +개발 운영 영역은 저장소의 변경과 검증을 통제하며 solver가 실행 중 이를 호출하지 않는다. -### Windows runtime closure +## 공통 책임과 의존성 -Configure 성공은 executable이 GoogleTest discovery 또는 CLI 실행 시 필요한 DLL을 찾는다는 -뜻이 아니다. `fesa_cli`와 세 test executable의 POST_BUILD 단계는 TBB, MKL thread/core/default -dispatch, OpenMP, `libmmd.dll`, HDF5를 포함한 imported-target runtime DLL을 executable 옆에 -복사한다. 새 dynamic backend를 도입할 때는 link 성공뿐 아니라 clean environment에서의 -post-build discovery/실행까지 runtime closure로 다뤄야 한다. 개인 설치 absolute path를 -CMake source에 기록하지 말고 config package와 imported target metadata를 확장한다. +아래 명칭은 책임 경계이며 반드시 각각 하나의 클래스나 독립 library를 뜻하지 않는다. +실제 타입과 파일 배치는 승인된 Feature와 현재 코드 구조에서 결정한다. -## 모듈 경계 -- `core`는 외부 라이브러리에 의존하지 않는다. -- `io/abaqus`는 syntax와 semantic mapping만 담당하고 해석 알고리즘을 알지 않는다. -- `model`은 Abaqus keyword 문자열이 아니라 solver semantic model을 가지며 `Domain`과 - non-owning `AnalysisModel`의 수명 경계를 소유한다. -- `materials`와 `properties`는 Domain이 소유하는 semantic identity와 현재 승인된 - isotropic elasticity 및 beam/shell property data만 제공한다. -- `elements`는 Domain-owned `ElementDefinition`, runtime numerical `Element`, checked - `ElementFactory`와 element-local stiffness/recovery contract를 제공한다. -- `fem`의 `DofManager`는 DOF, equation ordering, scatter와 sparse pattern을 소유한다. -- `assembly`는 runtime element contribution과 ordered load contribution을 stable full-DOF - space에 조립한다. Contribution producer는 global storage를 직접 갱신하지 않는다. -- `loads`는 semantic target과 magnitude를 소유하고 ordered `LoadContribution`을 생성한다. -- `constraints`는 `ConstraintDefinition` 생성과 V0 essential BC elimination 및 full/reduced - vector 변환을 분리한다. MPC와 penalty는 현재 범위가 아니다. -- `solvers`는 `LinearSolver` 뒤에 MKL PARDISO 세부 구현을 감춘다. TBB는 `assembly/ParallelFor`, HDF5는 `results/ResultsWriter` 경계 뒤에 각각 격리된다. -- `analysis`는 step/history data를 받아 procedure를 실행하고 solver backend와 result writer를 조율한다. -- `results`는 full residual과 B33/MITC4 rows를 복구하고 backend-neutral writer contract를 - 제공한다. HDF5 schema 구현은 `io/hdf5`가 담당한다. -- test helper는 production parser/solver 내부 상태를 우회하지 않는다. +| 경계 | 소유하는 책임 | 소유하지 않는 책임 | +| --- | --- | --- | +| Core identity/diagnostic | Source identity, 공통 상태와 진단의 의미 | 파일 문법, 물리 정식화, vendor resource | +| Input adapter | 외부 syntax와 지원 semantics의 검증·mapping | 해석 알고리즘과 acceptance 변경 | +| Model definition | Topology, 재료·property·하중·제약·step 정의와 stable identity | Equation 번호, 해석 중 mutable solution | +| Active model view | 현재 procedure/step에 필요한 모델의 non-owning 참조 | 원본 모델 복사나 수명 소유 | +| Element/material | 승인된 local ordering의 물리 응답과 recovery 계약 | Global equation numbering, 직접 sparse write, 파일 저장 | +| Load definition/evaluation | Target·magnitude·configuration 의미와 ordered contribution | Global vector의 공유 누적 | +| DOF/equation | 활성 DOF, full/free/constrained mapping, numbering, scatter와 sparse pattern | 요소 정식화, 수치 factorization | +| Assembly | Local contribution을 지정된 global structure에 결정적으로 누적 | Physics 재해석, equation 번호의 분산 생성 | +| Constraint policy | 승인된 제약의 equation 적용과 해 복구 의미 | Input syntax나 별도 물리 모델의 임의 도입 | +| Analysis procedure | 필요한 state·해법·반복/종료·commit·recovery lifecycle | Vendor handle이나 concrete storage API의 공개 | +| Numerical backend | 행렬·벡터 연산, 해법 resource와 backend failure translation | Source label의 물리 의미, 입력 모델 해석 | +| Results/recovery | 계산 결과의 물리량·위치·identity와 state 반영 | 저장 파일 layout이나 reference 데이터 보정 | +| Output adapter | 승인 schema로의 serialization, self-check와 finalization | 물리량 정의와 acceptance 판정 변경 | -## 승인된 리팩터링 dependency와 ownership +의존성은 consumer가 명시된 contract를 사용하는 방향으로 둔다. Input/output과 vendor +adapter가 core 계약에 의존하며, public core가 concrete adapter나 vendor/OS 타입에 +의존하지 않는다. Interface와 implementation의 의존성 방향을 역전하지 않는다. -다음 graph는 C++ object-oriented modular refactoring의 구현 방향을 고정한다. `owns`는 -단독 수명 소유권을, 나머지 화살표는 왼쪽 consumer가 오른쪽 contract를 사용한다는 -뜻이다. 이 graph는 기존 B33/MITC4 물리 기능을 늘리지 않는다. +Numerical consumer가 요소 종류별 concrete storage를 직접 분기하지 않도록 승인된 +evaluation/contribution contract를 사용한다. Definition과 runtime object의 호환성은 +지정된 생성 경계에서 검사한다. 이를 위해 speculative registry, 공유 소유권 또는 +사용하지 않는 polymorphic capability를 추가하지 않는다. -```text -Domain owns ElementDefinition / ElementProperty / Material / StepDefinition -AnalysisModel is a non-owning stable-index view into Domain -ElementFactory creates runtime Element candidates from compatible definitions -DofManager -> ElementDofLayout -SparseAssembler -> ElementStiffnessContribution -ResultRecovery -> ElementResultBundle -LoadAssembler -> ordered LoadContribution -EssentialConstraintPolicy -> ConstraintDefinition -Analysis <- LinearStaticAnalysis -``` +## 데이터 계약 -승인된 B33/MITC4 formulation, operation/reduction order, sign, units, coordinates와 row -identity는 이 ownership 리팩터링보다 우선한다. HDF5 schema, reference artifact와 -ADR-014/ADR-020 tolerance도 변경하지 않는다. +### Identity -`Domain`은 `ElementDefinition`, `ElementProperty`, `Material`과 `StepDefinition` base -object를 `std::unique_ptr`로 단독 소유하고 const access를 제공한다. Collection position은 -기존 stable `EntityIndex` 의미를 유지한다. `AnalysisModel`과 solver consumer는 ownership을 -가져오지 않고 Domain 수명 안에서 stable index 또는 const reference만 사용한다. -`std::shared_ptr`, speculative `Clone()`과 global registry는 이 계약에 포함되지 않는다. +다음 개념을 같은 정수나 row 위치라는 이유로 서로 대체하지 않는다. -`ElementDefinition`은 source identity, source element type, connectivity와 -property/material identity를 보존하는 semantic object다. Runtime `Element`는 active -`ElementDofLayout`, `ElementStiffnessContribution`과 `ElementResultBundle`을 제공하는 -numerical kernel이다. `ElementFactory`만 compatible definition/property/material 조합을 -검사해 runtime candidate를 만들며 unknown 또는 incompatible 조합을 기존 -`Status`/`Result` diagnostic으로 fail-closed 처리한다. `DofManager`, -`SparseAssembler`와 `ResultRecovery`는 B33/MITC4 concrete storage가 아니라 위 runtime -contract를 소비한다. Linear-static candidate는 runtime `Element`를 -`std::vector>`로 소유하고, consumer에는 그 owner 수명 안에서만 -유효한 non-owning `ElementView`를 제공한다. +- Source identity: 외부 입력에서 의미가 있는 label과 instance/scope +- Internal identity/index: 검증된 모델 내부의 안정된 식별 또는 위치 +- DOF identity: Entity의 물리 자유도와 component +- Equation index: 특정 procedure의 equation ordering +- Result identity: Entity, quantity, location, component, step/frame과 coordinate 의미 -`Material` base는 identity, source location과 수명 의미만 공유한다. 현재 concrete -isotropic linear elasticity에 필요한 capability만 사용하며 density, anisotropy, plastic -state, temperature 또는 rate dependency를 optional field나 no-op method로 미리 추가하지 -않는다. `ElementProperty`도 beam/shell이 실제 사용하는 data만 각 concrete type에 둔다. +Renumbering, filtering, 병렬 scheduling과 serialization 뒤에도 필요한 대응 관계를 +보존한다. Reference matching은 row 순서가 아니라 계약된 source identity와 component를 +사용한다. 특정 typed wrapper나 result-key 클래스를 이 문서만으로 새로 만들지 않는다. -`Load`는 semantic target과 magnitude를 소유하고 source order가 보존된 -`LoadContribution`을 생성한다. Global vector의 deterministic accumulation은 -`LoadAssembler`만 수행한다. `BoundaryCondition`은 `ConstraintDefinition`을 생성하고 -`EssentialConstraintPolicy`가 prescribed displacement의 stable elimination과 -reconstruction을 수행한다. Future distributed/body load와 MPC enforcement는 구현된 -기능이 아니며 별도 승인 계약 없이 이 경계에 branch나 optional state를 추가하지 않는다. +### Units, coordinates와 result meaning -책임이 큰 facade는 외부 계약을 유지한 채 private implementation만 다음 owner로 나눈다. +입력 값의 단위계를 임의로 추론하거나 결과에 맞춰 변환하지 않는다. 필요한 dimension, +좌표계, component 순서, 변환과 부호는 정식화 및 I/O 계약으로 고정한다. +서로 다른 위치나 정의의 recovery quantity를 같은 결과로 취급하지 않는다. +Averaging, extrapolation 또는 station 변환은 별도 승인된 의미가 있을 때만 수행한다. -- Abaqus Domain mapping: topology, material/property, step/load/boundary mapping과 final - Domain assembly -- Result recovery: global equilibrium, beam recovery, shell recovery와 atomic state commit -- HDF5 writing: RAII/primitives, model datasets, result datasets, self-check와 atomic - finalization +### 모델, view와 state의 수명 -이 분할은 parser diagnostic, `ResultsWriter` boundary, HDF5 schema 또는 final-file -atomicity를 변경하지 않는다. - -## V0 입력 경계 - -V0 parser는 keyword와 parameter를 case-insensitive하게 해석하되 source label의 원문을 -보존한다. 지원하는 model/procedure keyword는 다음과 같다. - -- `*HEADING` -- `*PART`, `*END PART` -- `*NODE` -- `*ELEMENT, TYPE=B33` -- `*NSET`, `*ELSET`, including `GENERATE` -- `*MATERIAL`, `*ELASTIC` -- `*BEAM GENERAL SECTION, SECTION=GENERAL` -- `*SECTION POINTS` -- `*ASSEMBLY`, `*END ASSEMBLY` -- `*INSTANCE`, `*END INSTANCE` -- `*BOUNDARY`, `*CLOAD` -- `*STEP`, `*STATIC`, `*END STEP` - -Part 내부 label은 `SourceEntityId { instance_name, source_label }`로 보존하고 Domain은 -별도 stable internal index를 부여한다. 같은 part의 여러 identity instance는 허용하지만 -translation/rotation data와 nested assembly는 구조화된 unsupported diagnostic으로 -거부한다. 입력 파일당 하나의 static step만 허용하고 두 번째 step은 오류다. - -`*PREPRINT`, `*RESTART`, `*TRANSVERSE SHEAR STIFFNESS`, `*OUTPUT, FIELD`, -`*OUTPUT, HISTORY`, `*NODE OUTPUT`, `*ELEMENT OUTPUT`, `*CONTACT OUTPUT` 및 그에 속한 -미지원 output variable data는 warning 후 no-op 처리한다. 이 allowlist 밖의 미지원 -model-affecting keyword는 오류다. FESA output은 Abaqus output request에 좌우되지 않는다. - -## V0 beam과 section 계약 - -`EulerBeam3D`는 2절점과 절점당 `[UX, UY, UZ, URX, URY, URZ]` 6 DOF를 사용하며 축, -두 방향 Euler–Bernoulli 굽힘과 Saint-Venant 비틀림을 제공한다. Beam tangent를 local -`x`, Abaqus first section axis `n1`을 local `y`, `t x n1`을 local `z`로 둔다. -`*BEAM GENERAL SECTION`의 `A, I11, I12, I22, J`는 `Iy=I11`, `Iz=I22`로 매핑하고 -V0는 `I12=0`만 허용한다. `E`, `G`, `A`, `Iy`, `Iz`, `J`는 양수여야 하며 zero-length -element와 tangent에 평행한 guide vector를 scale-aware tolerance로 거부한다. - -요소 API는 stiffness, transformation, load와 recovery 책임을 분리한다. - -```cpp -Matrix localStiffness() const; -Matrix globalStiffness() const; -Vector localEquivalentLoad(const ConstantLocalLineLoad& load) const; -BeamRecovery recover(const Vector& globalElementDisplacement) const; -``` - -`localEquivalentLoad`는 formulation의 constant local line-load kernel을 unit test하기 -위한 계약이다. V0 parser는 `*DLOAD`나 distributed-load Domain object를 생성하지 않으므로 -CLI pipeline에서는 이 kernel을 호출하지 않는다. Stiffness와 recovery는 formulation의 -2점 Gauss rule과 부호 규약을 따른다. - -## 핵심 객체 모델 - -```text -Domain -├── owns nodes, sets and source identity -├── owns unique_ptr -├── owns StepDefinition with Load / BoundaryCondition definitions -└── owns source path/identity and mapping warnings - -AnalysisModel -├── non-owning stable-index view into Domain -├── stable active element/BC/load definition indices -└── reachable material/property indices - -ElementFactory -└── creates checked runtime Element candidates - -Element -├── exposes ElementDofLayout -├── produces ElementStiffnessContribution -└── recovers ElementResultBundle - -DofManager -├── owns node x [UX,UY,UZ,URX,URY,URZ] full-DOF numbering -├── owns stable free/constrained order and prescribed values -├── owns element scatter maps -└── owns full-space CSR structural pattern - -AnalysisState -├── displacement U -├── external force Fext -├── internal force Fint -├── residual R and full-index reaction -├── StepFrameIdentity -└── endpoint, Gauss and S11 recovery rows - -Analysis -└── LinearStaticAnalysis - -Vector -Matrix -SparseMatrix -``` - -위 abstract boundary는 현재 B33/MITC4, isotropic linear elasticity, beam/shell property, -concentrated nodal load, prescribed displacement와 linear static procedure를 연결하는 데 -필요한 최소 계약이다. MITC3, solid, dynamic, frequency, heat-transfer와 plastic behavior는 -구현된 기능이 아니며 승인된 사용 사례 전에 future-only method나 state를 base에 추가하지 -않는다. - -## 상태 관리 -- `Domain`은 입력 파일에서 만들어진 전체 모델 정의를 `std::unique_ptr`로 단독 소유한다. - 파싱 이후에는 가능한 한 불변으로 취급하고 stable collection index를 바꾸지 않는다. -- `LinearStaticAnalysis`가 `Domain`을 소유하고, 그 뒤에 `AnalysisModel`, `DofManager`, `AnalysisState`, stiffness/RHS를 순서대로 만든다. 재사용 시에는 역순으로 해제하여 이전 Domain을 가리키는 view를 남기지 않는다. -- `AnalysisModel`은 현재 step에서 활성화되는 해석 객체들의 실행 view이다. `Domain`을 복사하지 않으므로 Domain이 반드시 더 오래 살아야 한다. -- `DofManager`는 자유도와 방정식 번호를 전담한다. `Node` 또는 `Element` 내부에 equation id를 분산 저장하지 않는다. -- `AnalysisState`는 V0 frame에 필요한 다섯 full-DOF vector와 recovery rows만 소유한다. Velocity, acceleration, temperature, iteration history, nonlinear element state는 해당 procedure가 승인될 때 별도 계약으로 추가한다. -- Result recovery는 모든 candidate vector/row를 검증한 뒤 state에 반영한다. 실패한 recovery가 앞선 유효 state를 부분적으로 덮어쓰지 않아야 한다. - -## 데이터 흐름 -```text -Abaqus input file --> syntax parse and semantic mapping --> immutable Domain 생성 --> 단일 step AnalysisModel view 생성 --> ElementFactory가 compatible definition에서 runtime Element candidate 생성 --> DofManager가 ElementDofLayout으로 DOF/scatter map/sparse pattern 생성 --> ElementStiffnessContribution의 deterministic COO-to-CSR 조립 --> ConstraintDefinition의 stable essential-constraint partition 생성 --> LinearSolver::factorize(Kff) --> ordered LoadContribution의 full nodal load vector 조립 --> effective RHS = Ff - Kfc * dc --> LinearSolver::solve(rhs, df) substitution --> full displacement 복구 --> full residual/reaction = K*d - F 및 ElementResultBundle 복구 --> ResultsWriter로 derived `_results.h5` atomic finalization -``` - -### 승인된 Nonlinear Static 경로 - -`PrepareAnalysisInput()`은 input syntax와 semantic Domain을 정확히 한 번 만든다. -`ExecutePreparedAnalysis()`는 sole static step의 procedure identity로 linear static 또는 -nonlinear static executor를 선택하며 두 procedure의 mutable state를 공유하지 않는다. -Nonlinear executor는 committed/trial `NonlinearAnalysisState`, full unsymmetric CSR tangent -solver와 closed-before-finalization Newton iteration log를 소유한다. Completion 뒤 -`FinalizePreparedAnalysisResult()`가 selected backend-neutral writer로 only accepted frames를 -atomic finalization한다. - -Nonlinear schema v1의 factor-one `/global/equilibrium`은 raw full-residual sum이 아니라 -global origin `[0,0,0]`에서 current `X+u` force arm과 independently spatialized applied and -masked constrained-reaction moments를 합한 applied-plus-reaction balance error다. Accepted -free residual은 generalized residual의 free mask projection이고 constrained-reaction resultant는 -별도 review summary다. 어느 quantity도 다른 quantity의 substitute가 아니다. - -강성행렬 factorization은 하중벡터 조립보다 먼저 수행한다. 반력은 element end action의 -별도 합이 아니라 조립된 전체 residual에서 구한다. Constrained component는 physical -reaction이고 free component는 equilibrium residual evidence로 full-index vector에 남긴다. +- `Domain`은 전체 모델 정의를 소유하며 검증 이후 가능한 한 불변으로 취급한다. +- `AnalysisModel`은 Domain을 복사하지 않는 non-owning 실행 view다. Domain은 모든 + view와 그에 의존하는 계산보다 오래 살아야 한다. +- `DofManager`는 DOF/equation mapping과 sparse pattern을 전담한다. Node나 Element에 + equation 번호를 분산 저장하지 않는다. +- Analysis owner는 해당 procedure가 실제로 사용하는 mutable state와 workspace만 소유한다. + 재사용·재시도·해제 시 dependent object를 남기지 않는다. +- Trial/committed 구분이 필요한 procedure는 update 실패와 commit 시점을 별도 계약으로 + 정의한다. 모든 procedure나 material에 이 state를 일괄 추가하지 않는다. +- Recovery와 output은 유효한 candidate를 검증한 뒤 반영한다. 기존 state와 최종 파일의 + 보존 범위는 성공 경로뿐 아니라 실패 경로에서도 검증한다. ## 해석 실행 흐름 -`Analysis` base는 procedure-specific protected hook을 정의하지 않고 최소 실행 계약인 -`Analysis::Run(const AnalysisRequest&)`만 제공한다. 다음 여덟 단계의 순서와 fail-fast -경계는 `LinearStaticAnalysis::Run()`의 private lifecycle이며 다른 procedure에 강제되지 -않는다. +공통적으로 외부 입력의 의미를 검증하고 실행 모델을 구성한 뒤, 선택된 procedure가 +equation과 state를 관리하여 결과를 복구·저장한다. 이것은 책임 수준의 설명이며 모든 +procedure에 동일한 반복문이나 고정된 단계 목록을 강제하는 Template Method가 아니다. -| 순서 | Private stage | 주요 작업과 생성되는 소유 객체 | 순서/실패 불변식 | -| --- | --- | --- | --- | -| 1 | `Initialize(request)` | `.inp` syntax read, semantic map, owned immutable `Domain`, sorted warnings | 이전 run의 dependent object를 역순으로 제거하고 parse/map 실패를 input category로 반환한다. | -| 2 | `BuildAnalysisModel()` | non-owning `AnalysisModel` view | Domain을 복사하지 않으며 Domain lifetime 안에서만 사용한다. | -| 3 | `BuildDofMapAndSparsePattern()` | `DofManager`, zero-initialized `AnalysisState` | Stable full/free/constrained numbering과 structural pattern을 한 소유자에게 둔다. | -| 4 | `AssembleAndPartitionStiffness()` | full CSR K와 `Kff/Kfc/Kcf/Kcc` | Element-local buffer를 deterministic하게 reduce하고 structural zero와 stable order를 보존한다. | -| 5 | `Factorize()` | retained `Kff` factorization | 모든 load assembly보다 먼저 호출한다. Valid fully constrained model의 `0 x 0 Kff`는 trivial success다. | -| 6 | `AssembleLoadsAndEffectiveRhs()` | full F와 `Ff-Kfc*dc` | Semantic load source order와 finite sum을 보존하며 solver를 호출하지 않는다. | -| 7 | `SubstituteAndReconstruct()` | free solution과 full displacement | Factorization을 재수행하지 않고 substitution한 뒤 prescribed value를 stable order로 복구한다. | -| 8 | `RecoverAndWriteResults()` | full residual/reaction, B33/MITC4 rows, final HDF5 | Recovery candidate를 원자적으로 commit하고 writer 성공 뒤에만 최종 output을 교체한다. | +각 procedure 계약은 다음을 명시한다. -비선형 정적 및 동적 해석은 V0 범위가 아니며 별도 ADR과 formulation을 승인한 뒤 이 -lifecycle과 state/equation 계약을 별도 procedure에 정의한다. LinearStaticAnalysis의 -private stage 사이에 조용히 반복·증분·시간 적분 동작을 삽입하지 않는다. +1. 입력 준비와 procedure 선택의 경계, 원본 모델의 owner와 lifetime +2. 필요한 DOF, state, equation, workspace와 초기화 조건 +3. Contribution, constraint, factorization/solve와 update의 순서 +4. 해당하는 경우 increment, iteration, convergence와 종료 조건 +5. 성공 candidate의 commit, recovery와 결과 frame의 의미 +6. 실패 category, rollback·재사용 조건과 final output의 보존 범위 -## 설계 패턴 -- Strategy/Adapter Pattern: `Analysis::Run(const AnalysisRequest&)`, `LinearSolver`, - `ParallelFor`, `ResultsWriter`가 현재 승인된 public 실행/backend 경계다. Vendor API는 - concrete adapter implementation 안에만 둔다. -- Procedure-owned lifecycle: `Analysis`는 protected Template Method hook을 공유하지 않고 - `LinearStaticAnalysis`가 승인된 여덟 단계 lifecycle을 private하게 소유한다. -- Syntax/Semantic separation: `AbaqusInputReader`는 syntax record를 만들고 `AbaqusDomainMapper`가 승인된 keyword 의미를 concrete Domain record로 변환한다. -- Runtime Polymorphism: backend와 승인된 analysis/element/material/property/load/boundary - 경계에만 사용한다. Factory compatibility는 중앙에서 fail-closed로 검사하며 global - registry 또는 future-only capability를 추가하지 않는다. -- RAII: MKL handle, HDF5 file/dataset, temporary solver workspace의 수명과 오류 처리를 wrapper에 묶는다. +정적·동적, 선형·비선형, eigen 또는 contact 계열은 실제 요구와 승인된 계약으로 +정의한다. 이 구분을 명시하는 것은 해당 기능이 이미 지원된다는 뜻이 아니다. +기존 절차에 조건문으로 새 physics를 끼워 넣거나, 간결한 공통 그림을 근거로 승인된 +연산 순서·reaction·residual·commit 의미를 바꾸지 않는다. -## Sparse Matrix Policy -- Assembly는 element마다 index-addressed contribution buffer를 만들고, join 뒤 COO tuple을 stable order로 정렬해 한 thread에서 순서대로 합산한 후 CSR로 finalize한다. -- `SparseMatrix`는 0-based CSR 데이터를 소유하는 별도 타입이며 dense `Matrix`를 - 상속하지 않는다. MKL PARDISO backend는 adapter 경계에서 필요한 descriptor와 indexing을 - 변환한다. -- CSR row offset, sorted-unique column, dimensions, finite values를 construction boundary에서 검증하며 structural zero를 삭제하지 않는다. -- Parallel worker는 global sparse matrix나 shared reduction accumulator를 직접 갱신하지 않는다. Worker별 failure slot과 contribution만 쓰고, deterministic reduction은 join 뒤에 수행한다. -- Matrix symmetry와 factorization/substitution failure는 solver adapter가 구조화된 solver diagnostic으로 번역한다. 승인되지 않은 regularization이나 fallback으로 singularity를 숨기지 않는다. +현재 개별 procedure의 구체적인 실행 계약은 해당 Feature의 formulation·I/O·구현 문서를 +확인한다. 기존 프로젝트 문서의 실행 순서와 객체 배치는 +[개정 전 아키텍처](archive/pre-generalization/ARCHITECTURE.md)에 이력으로 보존한다. -## Dense Math Policy +## Local 계산, assembly와 constraint -- `Vector`는 contiguous `double` 데이터와 크기를 소유하고 copy, dot, Euclidean norm, - scale, axpy를 MKL CBLAS adapter로 수행한다. -- `Matrix`는 row-major contiguous `double` 데이터와 dimensions를 소유하고 - matrix-vector 및 matrix-matrix 연산에 `CBLAS_ROW_MAJOR`를 사용한다. -- 두 타입은 copy/move semantics와 bounds-checked access를 제공한다. -- MKL header와 MKL-specific type은 adapter implementation 밖으로 노출하지 않는다. +Local kernel은 승인된 geometry, material/property, DOF/state와 요청된 계산 범위를 받아 +local ordering이 명시된 contribution 또는 recovery 값을 만든다. Global numbering, +공유 matrix 갱신, 파일 lookup, 전체 convergence 판정이나 최종 저장을 수행하지 않는다. -## Parallel Policy -- `ParallelFor`는 `[0,count)` index-addressed 독립 작업만 노출하며 `SerialParallelFor`와 `TbbParallelFor`가 같은 observable contract를 가진다. -- 첫 oneTBB 적용 지점은 element-local stiffness 계산이다. 각 callback은 자기 index의 output slot만 쓴다. -- 전역 sparse write와 부동소수 reduction은 parallel callback 밖의 deterministic 단계로 제한한다. -- MKL 내부 thread와 TBB element loop가 oversubscription을 만들지 않도록 thread count와 task arena 정책을 명시한다. +DOF owner가 지정한 pattern과 scatter를 assembly가 사용한다. Contribution 생성과 +누적 순서를 명시하고 병렬 worker는 소유한 buffer에만 기록한다. Join 이후 reduction과 +diagnostic 선택은 결정적으로 수행한다. Hash iteration이나 실행 scheduling을 +authoritative ordering으로 사용하지 않는다. -## HDF5 Result Schema -```text -/metadata -/model/nodes -/model/elements -/steps//frames/0/nodal/displacement -/steps//frames/0/nodal/reaction -/steps//frames/0/element/end_force_local -/steps//frames/0/element/section_resultant -/steps//frames/0/element/generalized_strain -/steps//frames/0/element/stress_s11 -/diagnostics -``` +Constraint 방식, full/reduced space, prescribed value, residual과 reaction의 정의는 +선택된 procedure의 수학 계약에 포함한다. 서로 다른 enforcement 방식의 결과 의미를 +같다고 가정하지 않는다. 특수 차원·빈 시스템·singularity의 처리는 계약에 따라 검증하며 +임의의 regularization, zero clamp 또는 fallback으로 실패를 숨기지 않는다. -Schema requirements: -- schema version, units, coordinate system, solver version, source input identity를 metadata에 기록한다. -- field output과 history output을 구분한다. -- reference comparison을 위한 row identity는 node id, element id, integration point id, step/frame id를 포함한다. -- FESA solver는 input lexical sibling `_results.h5`를 authoritative output으로 쓰며, - 같은 directory의 `_log.txt`는 procedure log다. -- Abaqus reference results는 기능 문서가 선언한 기존 CSV 파일이다. Directory/file - naming, README, metadata 또는 provenance는 architecture readiness 조건이 아니다. -- Verification은 기능이 요구하는 source identity와 component를 결정적으로 대응시키고 - 승인 tolerance를 적용한다. Missing/extra/duplicate/nonfinite required row는 숫자 비교 - 전에 실패한다. 단일 step/final-frame 기능은 별도 CSV step/frame 열을 요구하지 않는다. -- FESA HDF5에서 추출한 deterministic CSV view는 optional debugging/review artifact이며 공식 solver output 또는 reference artifact가 아니다. -- Writer는 final과 같은 directory의 임시 HDF5에 전체 schema를 쓴 뒤 flush, checked close, - read-only reopen/self-check를 수행한다. Existing final은 `ReplaceFileW`, 새 final은 - `MoveFileExW(..., MOVEFILE_WRITE_THROUGH)`로 완료하며 실패 시 temporary artifact를 - 정리하고 불완전한 파일을 정상 `_results.h5`로 노출하지 않는다. +## Numerical backend와 실행 환경 -## V0 결과 복구와 reference normalization +Math storage, index convention, matrix capability, resource lifetime과 오류 변환은 +backend-neutral 계약으로 노출한다. Vendor header, handle과 OS 타입은 private adapter에 +격리한다. Dense와 sparse의 의미가 다르면 하나의 storage hierarchy로 억지로 합치지 않는다. -- Nodal displacement와 reaction은 global `[UX, UY, UZ, URX, URY, URZ]` 순서다. -- Equilibrium end action은 local `[FX,FY,FZ,MX,MY,MZ]`, endpoint section resultant는 - `[N,T,My,Mz]`, generalized strain/resultant는 두 Gauss point에 기록한다. -- General beam section stress는 section point의 axial `S11`만 복구한다. Section point가 - 없으면 centroid `(0,0)`을 `source=fesa-default`로 기록한다. -- 승인된 `reference/cantilever beam/cantilever beam elemental forces.csv`는 각 B33 - element의 두 endpoint에 `Element Label`, `Node Label`과 `SF1/SM1/SM2/SM3`을 제공한다. - `(instance, element label, endpoint node label, component)` identity로 HDF5 - `[element,endpoint,N/T/My/Mz]`에 직접 대응하고 `SF1 -> N`, `SM1 -> My`, `SM2 -> Mz`, - `SM3 -> T`로 비교한다. External reference comparison은 node-station collapse나 평균을 - 사용하지 않는다. -- Reference tolerance는 같은 model/case, step/frame, logical quantity, unit dimension, - coordinate system과 blocking behavior의 component family에서 Abaqus-only scale - `S=max(abs(reference))`를 구한다. `abs(reference)<=0.01*S`인 행은 absolute error - `<=0.01*S`, 그 외 행은 relative error `<=0.05`로 판정하며 family scale-relative RMS - `RMS(error)/S<=0.01`도 통과해야 한다. 독립 absolute-error gate는 사용하지 않고 - zero-scale family는 FESA도 exact zero일 때만 통과한다. 전체 판정 과정과 report/change - management contract는 `docs/TOLERANCE.md`를 따른다. -- Beam stress는 HDF5 schema와 unit/analytical test로 검증하지만 Abaqus reference - comparison은 N/A다. +Build dependency는 프로젝트가 소유한 target 경계에서 정규화하고, 새로운 dynamic +dependency는 link뿐 아니라 실제 실행에 필요한 runtime closure까지 확인한다. +개인 설치 절대 경로를 소스 계약으로 고정하지 않는다. Thread와 workspace 정책은 +실제 workload와 재현성 evidence에 근거하여 관리한다. -## CLI와 diagnostics +현재 target graph와 dependency 선택은 [CMakeLists.txt](../CMakeLists.txt), +[dependency 설정](../cmake/FesaDependencies.cmake), +[solver target](../src/fesa/CMakeLists.txt), [test target](../tests/CMakeLists.txt)과 +[CMakePresets.json](../CMakePresets.json)을 확인한다. 이 참조는 다른 toolchain이나 +backend를 자동 승인하지 않으며 FESA의 현재 검증 baseline은 프로젝트 지침을 따른다. -```powershell -fesa.exe -``` +## 입력, 결과와 진단 -CLI는 input과 같은 lexical sibling directory에 `_results.h5`와 -`_log.txt`를 유도하고 `--output`을 거부한다. Exit code는 -`0=success`, `2=usage`, `3=input syntax/semantic mapping`, `4=model validation`, -`5=factorization/substitution`, `6=HDF5 output`으로 고정한다. Diagnostic은 `severity`, -`code`, `file`, `line`, `keyword`, `entity_identity`, `message`를 가지며 stderr에 -deterministic한 순서로 출력한다. +Input adapter는 지원되는 외부 syntax, scope, default와 model/history 의미를 +검증된 semantic model로 mapping한다. 의미상 무해하다고 계약된 항목만 warning/no-op으로 +처리할 수 있으며 나머지 미지원 semantics는 fail-closed로 진단한다. -## 기능 확장 플레이북 +Results/recovery가 물리량의 의미를, output adapter가 저장 layout을 소유한다. +Authoritative output과 diagnostic log, debug view, reference artifact를 구분한다. +공식 형식, 파일명, schema version, mandatory dataset과 backward compatibility는 +현재 프로젝트·Feature I/O 계약을 따른다. 일반화 때문에 기존의 필수 결과를 optional로 +바꾸거나 다른 파일 형식으로 대체하지 않는다. -다음 표는 코드 위치만이 아니라 함께 바뀌어야 하는 계약 경계를 나타낸다. 한 열만 -구현하고 다른 열을 생략하면 internal utility 또는 실험 kernel일 수는 있어도 제품 기능은 -아니다. +Output은 계약된 임시 candidate에 기록하고 검증·close를 완료한 뒤 최종 결과로 반영한다. +실패 frame이나 불완전한 파일을 정상 결과처럼 공개하지 않는다. 내부 state의 commit과 +외부 파일의 finalization은 서로 다른 경계이므로 각각의 failure contract를 정의한다. -| 기능 유형 | 시작 전에 고정할 것 | 주요 구현 경계 | 함께 검증할 것 | 피해야 할 shortcut | -| --- | --- | --- | --- | --- | -| 새 element/material | DOF, interpolation, constitutive law, integration, local axes/sign, feature-approved validity boundary | Domain record와 mapper, element kernel, DofManager scatter/pattern, SparseAssembler, ResultRecovery | feature-required invariants/tests and blocking reference quantities | 서로 다른 물리를 같다고 주장, source ID와 internal index 혼용, 검증 전 범용 hierarchy 추가 | -| 새 load/constraint | Abaqus target grammar, application order, units, follower 여부, prescribed-value 의미 | Syntax/mapper, Domain target, full-space LoadAssembler 또는 constraint partition, diagnostics | set/direct target ambiguity, multi-instance identity, nonfinite sum, `Ff-Kfc*dc`, reaction | Element load kernel 존재를 parser 지원으로 간주, penalty를 elimination에 몰래 혼합 | -| 새 analysis procedure | governing equation, state variables, increment/time lifecycle, tangent/residual, convergence와 output frame | 별도 Analysis implementation, procedure-specific state/equation owner, solver interface extension | orchestration order, failure atomicity, restart/frame identity, numerical benchmark | V0 hook 사이에 조건문으로 반복/시간 적분 삽입, 사용하지 않는 future state 선할당 | -| 새 numerical backend | matrix/index contract, lifecycle, reusable state, failure taxonomy, thread/runtime policy | 기존 `LinearSolver` 또는 `ParallelFor` interface의 concrete adapter, CMake normalized target | empty/dimension/extreme-scale input, repeated call, failed-output preservation, clean runtime discovery | Vendor type을 public header에 노출, silent fallback/regularization, absolute install path 고정 | -| 새 output/reference quantity | 물리 정의, location, sign, units, coordinates, stable row identity, mandatory 여부, tolerance | Result record/recovery, AnalysisState, ResultsWriter/HDF5 schema, comparator projection | schema dtype/shape, ordering, nonfinite rejection, identity inventory, reference N/A 대체 evidence | 서로 다른 result identity 혼합, station mismatch 평균, output request로 mandatory result 제거 | +Diagnostic은 실패 단계, 안정된 code와 메시지, 가능한 source location/entity를 제공한다. +정확한 category, field와 CLI exit code는 I/O 계약을 따른다. Backend 오류를 그대로 +누출하거나 내부 failure classification을 새 public exit code로 임의 승격하지 않는다. -모든 확장은 PRD의 제품 완료 정의와 요구조건→정식화→I/O→구현→reference→physics gate를 -따른다. 기존 feature contract에 없는 범위를 편의상 “Abaqus compatible”이라고 넓히지 않는다. +## 검증과 reference 경계 -## MITC4 확장 경계 +검증 coverage와 비교량의 논리적 정의는 Numerical Review, 물리량의 저장·외부 대응은 +I/O, 실행 evidence는 Implementation, 독립 물리 판단은 Physics Evaluation이 소유한다. +각 단계는 상위 계약을 결과에 맞춰 수정하지 않는다. -MITC4가 구현될 때 Abaqus `S4`와 `S4R` source type은 같은 FESA formulation을 선택한다. -Source type은 metadata/diagnostic identity로 보존하지만 FESA integration 또는 hourglass -경로를 선택하지 않는다. 6-DOF embedding의 비물리 drilling coordinate에는 physical -rotational stiffness block의 positive minimum diagonal에 `1e-3`을 곱한 고정 numerical -stabilization만 둔다. Drilling calibration, artificial-energy policy와 별도 drilling result -dataset은 이 기능 범위가 아니다. +- Source identity, location과 component 일치 여부를 수치 tolerance보다 먼저 확인한다. +- Missing, extra, duplicate와 nonfinite required value를 숨기지 않는다. +- 외부 reference 판정은 [TOLERANCE](TOLERANCE.md)의 공통 정책을 적용한다. 물리 평형, + 정식화와 수렴의 검증 기준은 각자의 계약을 따른다. +- Reference artifact의 naming이나 보조 metadata를 맞추기 위해 파일을 변경하지 않는다. +- 공개 입력 의미나 관찰 가능한 결과 비교를 외부 solver의 내부 동작 동등성으로 해석하지 않는다. -Full-integration FESA-MITC4의 reference comparison은 `reference/shell/` S4의 기존 input 및 -displacement CSV만 사용한다. Global `U1/U2/U3` translation family만 blocking이고 -`UR1/UR2/UR3` rotation family는 warning-only evidence다. 두 family 모두 ADR-022의 공통 -family-scale row/RMS 정책을 사용한다. S4R은 -같은 kernel을 선택하는 source mapping과 metadata를 unit/integration tests로 검증하며 -`reference/shellR/` artifact는 acceptance comparison에 포함하지 않는다. +구현 존재, 특정 revision의 검증 통과와 현재 release readiness는 별도로 확인한다. +Architecture 문서의 그림이나 모듈명이 기능의 완료 증거를 대신하지 않는다. -Approved nonlinear MITC4 verification은 `reference/shellNL-meshfine/` fine primary, -`reference/shellNL/` coarse companion과 `reference/shell/` linear control을 각각 독립 -copied-input directory에서 실행한다. Factor-one global force/moment balance와 accepted free -residual은 blocking internal evidence다. External `U1/U2/U3`는 blocking, `UR1/UR2/UR3`는 -warning-only이며 local nodal `RF1/RF2/RF3/RM1/RM2`는 deterministic review-only evidence다; -`RM3`와 stress는 nonlinear numerical comparison에서 제외한다. +## 개발 운영 인프라 + +공통 agent 상태 전이는 [SOLVER_AGENT_DESIGN](SOLVER_AGENT_DESIGN.md), skill의 역할은 +[SOLVER_SKILL_DESIGN](SOLVER_SKILL_DESIGN.md)을 따른다. 실제 실행 절차는 +[Harness skill](../.agents/skills/harness/SKILL.md)과 현재 agent/skill profile을 읽는다. + +| Owner | 책임 | +| --- | --- | +| Coordinator | Feature 문서 목록·상태, dispatch, evidence gate, revision·retry budget과 closure | +| Specialist | 할당된 stage의 전문 판단, 기본·상세 산출물과 공통 handoff | +| Planning Agent | 할당된 implementation plan과 phase/index/step 계획 파일 | +| Executor | 현재 step 선택, branch, transport attempt, timestamp, path-scoped commit과 phase 진행 | +| Implementation Agent | 선택된 step의 코드·테스트·보고서와 result payload | +| Hook | 도구 호출 전 guardrail과 설정된 종료 검증; TDD 관찰이나 완전한 sandbox의 대체가 아님 | + +Coordinator만 workflow를 전이한다. Specialist의 outcome은 전이 요청이고, Harness의 +step 상태는 별개의 실행 상태다. Executor transport attempt는 최대 2회이며 동일 +`retry_key`의 correction 1회와 뒤따르는 Implementation rerun은 Coordinator가 관리한다. +두 budget을 혼용하거나 실패 문구만 바꿔 반복하지 않는다. + +승인된 scope와 할당된 작업 안의 일반 계획·문서·local 실행에는 반복 승인 gate를 +추가하지 않는다. 계획 요청만으로 실행을 시작하지 않으며 외부 solver, 원격·파괴 작업과 +sandbox 권한 확대에는 해당 명시적 권한이 필요하다. + +Project config, preset과 실행 command는 검토해야 하는 실행 입력이다. Hook 통과나 +구조화된 argument list만으로 그 side effect가 격리되었다고 간주하지 않는다. +Executor는 시작 전 선택된 phase 제어 파일 밖의 변경을 거부하며 모든 pending Step의 +Planning 소유 `owned_paths`를 검증한다. 매 시도 후와 stage/commit 경계에서 신뢰된 목록과 +변경·staged 경로를 대조하고 구현 변경과 phase metadata를 분리한다. 범위 밖 변경은 +보존하고 재시도·커밋·다음 Step을 중단한다. 이 검사는 동시 writer의 원자적 격리나 여러 +커밋의 단일 transaction을 보장하지 않는다. 실행 중 동시 편집을 피한다. +종료 코드와 Step 보고 상태를 함께 판정하며 명시적 실패를 transport 재시도로 덮어쓰지 +않는다. 실제 wrapper timeout이고 결과 미보고일 때만 최초 포함 최대 두 번 시도한다. +환경·실패 진단과 원래 보고는 Executor 소유 `attempt_history`에 남기고 재개 때 보존한다. +등록된 Hook을 수동 실행하여 검증 evidence를 대신하지 않는다. + +문서 전용 Stop 생략은 기록된 동일 session/turn의 허용된 편집 대상이 모두 저장소 내부 +Markdown인 조건에 한정된다. 기록이 없거나 불완전한 읽기 전용 작업과 shell 쓰기까지 +안전하게 분류한다고 가정하지 않는다. 생략은 build/test 통과가 아니다. +사용자가 실행을 제외한 문서 작업은 정적 검토와 미실행 항목을 보고한다. + +## 문서와 아키텍처의 변경 + +프로젝트 공통 원칙의 변경은 공통 ADR로, 구현 항목별 선택은 해당 Feature ADR로 +기록한다. Feature PRD는 범위와 acceptance 요약을, requirements·formulation·I/O는 +전문 계약의 원본을 소유한다. 관리 문서와 기본·상세 산출물은 +[FEATURE_DOCUMENTATION](FEATURE_DOCUMENTATION.md)의 소유권과 링크 규칙을 따른다. + +Public dependency, identity, 수명, result meaning이나 lifecycle 변경은 영향받는 +계약과 evidence를 함께 검토한다. 상위 계약 변경으로 영향받는 evidence는 stale 처리하며, +문서 정리만으로 기존 기능의 재검증이나 release 승인을 완료한 것으로 표시하지 않는다. + +개정 전 구현 설명은 [보존본](archive/pre-generalization/ARCHITECTURE.md)에 남긴다. +해당 설명을 Feature 문서로 이관할 때 원래 범위와 승인 revision을 확인한다. +현재 문서는 과거 구현 상세를 삭제하여 계약을 없애거나 미래 API를 선결정하지 않는다. diff --git a/docs/CODINGSTYLE.md b/docs/CODINGSTYLE.md index 8812ed3..e897acd 100644 --- a/docs/CODINGSTYLE.md +++ b/docs/CODINGSTYLE.md @@ -26,7 +26,7 @@ ## Language와 Toolchain - Production language는 C++17 이상이며 MSVC x64를 지원해야 한다. -- 승인된 build 기준은 CMake, Visual Studio generator와 Debug configuration이다. +- 승인된 build 기준은 CMake, MSVC x64와 Debug configuration이다. Generator와 명령은 현재 `CMakePresets.json` 및 선택된 Harness 설정을 따르며, 저장소 preset은 MSVC/Ninja를 사용한다. - C++ compiler extension에 의존하지 않는다. - MKL, TBB, HDF5와 Win32 type은 public solver core header에 노출하지 않는다. - Standard library와 RAII를 manual lifetime management보다 우선한다. diff --git a/docs/HARNESS.md b/docs/HARNESS.md index afc5a28..f6c90e3 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -2,8 +2,15 @@ ## Requirements -Windows, Python 3.10 이상, Codex CLI가 필요하다. CMake 프로젝트에는 Visual Studio의 -Desktop development with C++ 워크로드와 MSBuild, CMake/CTest를 설치한다. +Windows와 Codex CLI가 필요하다. Executor는 Python 3.10 이상을 사용하며, 이 저장소의 +계약 테스트는 표준 `tomllib`을 사용하므로 검증 환경은 Python 3.11 이상으로 준비한다. +FESA는 Visual Studio의 Desktop development with C++ 워크로드, CMake/CTest와 Ninja를 +사용한다. MSVC x64 developer 환경과 dependency 경로는 [AGENTS.md](../AGENTS.md)의 +최소 검증 진입점을 따른다. + +역할·승인·상태 전이는 [Agent 계약](SOLVER_AGENT_DESIGN.md)과 +[Harness skill](../.agents/skills/harness/SKILL.md)을 따른다. 이 문서는 설치·실행 참고이며 +solver Feature의 9개 stage와 evidence gate를 대신하지 않는다. ## 프로젝트 자동 감지 @@ -19,27 +26,44 @@ CMake metadata, 하나의 `.sln`, 하나의 `.vcxproj` 순서다. C/C++가 아 Copy-Item .harness/config.example.json .harness/config.json ``` -계획을 승인해 phase 파일을 만든 뒤 Executor를 실행한다. +승인된 Feature와 Coordinator dispatch에 따라 Planning Agent가 할당된 phase 파일을 +작성한다. 일반 계획 파일 작성에 반복 승인을 요구하지 않는다. 실행까지 할당된 경우에만 +다음 명령을 사용하며, 계획 작성 요청만으로 Executor를 시작하지 않는다. ```powershell python scripts/execute.py -python scripts/execute.py --push ``` +`--push`는 해당 원격 push를 사용자가 명시적으로 승인했을 때만 추가한다. 실행 전 +Git top-level은 Harness root와 같고 선택된 phase 제어 파일 밖의 worktree는 clean이어야 +한다. 실행 중 동시 편집과 커밋 범위의 한계는 [동작 과정](HARNESS_WORKFLOW.md)을 따른다. + +모든 `pending` Step에는 승인된 정확한 상대 파일 목록인 `owned_paths`가 필요하다. +누락된 기존 phase는 Planning의 할당된 준비 작업에서 목록을 보완한 뒤 실행한다. +`[]`는 구현 파일 변경이 없음을 뜻하며 완료 이력에는 목록을 소급 추가하지 않는다. +Executor는 실행 후와 staging/commit 경계에서 소유권을 검사하며 범위 밖 변경은 +보존하고 중단한다. 상세 schema와 제한은 [Harness skill](../.agents/skills/harness/SKILL.md)을 따른다. + Executor가 시작하는 Codex 세션은 기본적으로 `workspace-write` sandbox를 사용한다. Windows native sandbox에서 MSVC compiler-id의 `cl.exe`가 정지하는 것이 재현되고 같은 명령이 sandbox 밖에서 정상 완료되는 경우에만, 사용자 승인을 받은 격리된 clean -worktree에서 다음처럼 해당 실행에 한정해 fallback을 지정할 수 있다. +linked Git worktree에서 다음처럼 해당 실행에 한정해 fallback을 지정할 수 있다. ```powershell -$env:FESA_HARNESS_CODEX_SANDBOX = "danger-full-access" -python scripts/execute.py -Remove-Item Env:FESA_HARNESS_CODEX_SANDBOX +$env:FEM_HARNESS_CODEX_SANDBOX = "danger-full-access" +python scripts/execute.py --approve-danger-full-access +Remove-Item Env:FEM_HARNESS_CODEX_SANDBOX ``` 허용값은 `workspace-write`와 `danger-full-access`뿐이다. 후자는 Codex Step에 workspace 밖의 파일 접근 권한도 부여하므로 일반 기본값으로 설정하지 않는다. 어느 모드에서도 -`.codex/hooks.json`의 PreToolUse와 Stop hook은 자동으로 실행된다. +`.codex/hooks.json`의 등록된 hook을 비활성화하지 않는다. 환경변수만으로 권한이 승인되지 +않으며 Executor는 승인 flag가 없거나 primary/dirty worktree인 권한 상승 실행을 거부한다. + +Stop은 유효한 동일 session/turn 편집 기록의 대상이 모두 저장소 내부 `.md`일 때만 +build/test를 생략한다. 기록 누락·손상 또는 비문서 편집은 정상 검증 경로를 따른다. +Shell 쓰기나 편집 기록 없는 읽기 전용 요청도 자동 생략을 보장하지 않는다. 문서 작업에서 +사용자가 실행을 제외하면 수동으로 Hook·build/test를 실행하지 않고 미검증 사실을 보고한다. ## Harness Python 검증 @@ -47,7 +71,7 @@ Remove-Item Env:FESA_HARNESS_CODEX_SANDBOX 다음 명령으로 실행한다. ```powershell -uv run --with pytest python -m pytest -v -rs +uv run --with pytest python -B -m pytest -v -rs ``` ## CMake preset 설정 @@ -62,21 +86,25 @@ uv run --with pytest python -m pytest -v -rs "projectType": "cmake", "cmake": { "sourceDir": ".", - "binaryDir": "out/build/windows-debug", - "configurePreset": "windows-debug", - "buildPreset": "windows-debug", - "testPreset": "windows-debug" + "binaryDir": ".harness/build-ninja-msvc-debug", + "configurePreset": "windows-msvc-ninja-debug", + "buildPreset": "windows-msvc-ninja-debug", + "testPreset": "windows-msvc-ninja-debug" } } ``` ```powershell -cmake --preset windows-debug -cmake --build --preset windows-debug -ctest --preset windows-debug --output-on-failure +cmake --preset windows-msvc-ninja-debug +cmake --build --preset windows-msvc-ninja-debug +ctest --preset windows-msvc-ninja-debug --show-only=json-v1 +ctest --preset windows-msvc-ninja-debug --output-on-failure ``` -Preset을 쓰지 않는 경우에는 같은 격리된 build directory를 명시한다. +위 예시는 현재 [CMakePresets.json](../CMakePresets.json)과 +[config 예시](../.harness/config.example.json)에 맞춘다. Preset 파일이 존재하는 것만으로 +Stop이 그 preset을 선택하지는 않는다. 아래는 preset을 쓰지 않는 adapter의 명령 형태이며, +FESA에서는 dependency 경로와 승인된 build 구성을 함께 확인한다. ```powershell cmake -S . -B .harness/build -A x64 @@ -87,6 +115,9 @@ ctest --test-dir .harness/build -C Debug --output-on-failure ## 직접 MSBuild 설정 +다음은 재사용 가능한 Harness adapter의 예시다. FESA 제품 검증은 CMake/CTest를 사용하며 +이 예시로 solution-only MSBuild 검증을 대체 승인하지 않는다. + `projectType`을 `msbuild`로 설정하면 `msbuild.solution`, `configuration`, `platform`을 지정한다. 직접 MSBuild 프로젝트에서는 `msbuild.testCommand`가 필수이며, 테스트 실행 파일과 인수를 JSON 배열로 적는다. @@ -129,11 +160,21 @@ MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64 ## 실패 복구 +Executor는 명시적 `error/blocked`를 비정상 종료에서도 보존하며 자동 재시도하지 않는다. +인식된 sandbox helper 환경 오류는 종료 코드 0에서도 Step과 상위 phase를 `blocked`로 +기록한다. 실제 wrapper timeout으로 결과를 받지 못했을 때만 최초 포함 최대 두 번 시도한다. +원인 미분류 nonzero와 `completed`/nonzero 불일치는 `error`다. 환경 차단은 종료 코드 2, +일반 실패는 1이며 preflight 실패는 branch/state 변경 전에 반환한다. + - Visual Studio C++ workload가 없으면 Installer에서 Desktop development with C++를 설치한 뒤 다시 실행한다. - solution 또는 project가 여러 개라서 모호하면 `projectType`과 `msbuild.solution`을 명시한다. - MSVC가 아닌 컴파일러가 감지되면 MSVC Developer Command Prompt에서 실행하거나 toolchain을 MSVC로 전환한다. - CTest가 0개 테스트를 보고하면 `enable_testing()`과 테스트 등록을 확인한다. - 직접 MSBuild 구성에 test command가 없으면 `msbuild.testCommand` 배열을 추가한다. - timeout 또는 명령 실패 시 Stop 응답의 stage, 안전한 argv 배열, 작업 디렉터리, - 종료 코드와 출력 tail을 확인하고 해당 명령을 단독으로 다시 실행한다. Harness는 - 별도의 로그 파일을 만들지 않는다. + 종료 코드와 출력 tail을 확인한다. 실행이 할당된 범위에서만 재현 명령을 수행하며 + 문서 작업의 검증 제외 지시를 덮어쓰지 않는다. Stop은 별도의 빌드 로그 파일을 만들지 않는다. +- 실패 분류와 재시작 결정은 Coordinator가 기록한다. Planning Agent는 명시적으로 할당된 + 복구 작업에서만 현재 실패 Step의 status를 `pending`으로 바꾸고, Executor가 재시작 시 + 오래된 outcome 필드와 timestamp를 정리하되 `attempt_history`는 보존한다. 실패 evidence를 먼저 보존하며 + 상세 절차는 [Harness skill](../.agents/skills/harness/SKILL.md)의 Recovery and handoff를 따른다. diff --git a/docs/HARNESS_WORKFLOW.md b/docs/HARNESS_WORKFLOW.md index feb3b2f..1569ba4 100644 --- a/docs/HARNESS_WORKFLOW.md +++ b/docs/HARNESS_WORKFLOW.md @@ -4,47 +4,57 @@ Codex 세션에서 Step을 실행하고, MSVC로 C++ 프로젝트를 검증하는 전체 과정을 설명한다. 설치 및 설정 예시는 [Harness 운영 가이드](HARNESS.md)를 참고한다. +Feature의 9개 정규 stage와 Coordinator의 gate 전이는 +[Agent 계약](SOLVER_AGENT_DESIGN.md), 역할별 절차는 +[Harness skill](../.agents/skills/harness/SKILL.md), 문서 소유권은 +[Feature 문서 관리](FEATURE_DOCUMENTATION.md)를 따른다. Harness는 그중 할당된 +구현 계획·Step 실행을 지원하며, solver workflow 전체를 대신하지 않는다. + ## 1. 핵심 구조 Harness Framework는 다음 세 계층으로 구성된다. -1. **계획 계층**: 요구사항을 분석하고 사용자가 승인할 실행 가능한 Step으로 변환한다. +1. **계획 계층**: 승인된 요구사항과 Coordinator dispatch를 실행 가능한 Step으로 변환한다. 2. **실행 계층**: Step Executor가 Step마다 독립 Codex 세션을 실행하고 상태와 Git 커밋을 관리한다. 3. **검증 계층**: PreToolUse 훅이 편집 전 정책을 검사하고, Stop 훅이 종료 전 MSVC - 빌드와 테스트를 실행한다. + 빌드와 테스트를 실행한다. 유효한 문서 전용 편집 기록이면 조건부로 생략한다. 전체 흐름은 다음과 같다. ```text 사용자 요구사항 ↓ -프로젝트 탐색 및 요구사항 논의 +Feature의 선행 계약·gate 확인 ↓ -Step 초안 작성 +Coordinator의 bounded Planning dispatch ↓ -사용자 승인 +Planning Agent의 자기완결적 Step 작성 ↓ phases/index.json, task index, stepN.md 생성 ↓ -Step Executor 시작 +실행까지 할당된 경우 Step Executor 시작 ↓ 각 Step을 독립 Codex 세션에서 실행 ├─ 도구 호출 전: PreToolUse 정책 검사 - └─ 응답 종료 전: Stop MSVC 빌드·테스트 + └─ 응답 종료 전: Stop 검증 또는 문서 전용 조건부 생략 ↓ 성공: 커밋 후 다음 Step -실패: 수정 또는 최대 3회 재시도 -차단: 사용자 개입을 기다리며 중단 +transport 실패: 최초 포함 최대 2회 시도 +구현 실패·차단: Coordinator로 반환 ``` -자연어 요구사항만으로 Executor가 자동 시작되지는 않는다. 계획을 사용자가 승인하고 -phase 파일을 생성한 다음 `scripts/execute.py`를 실행해야 구현 루프가 시작된다. +자연어 요구사항이나 계획 요청만으로 Executor가 자동 시작되지는 않는다. 승인된 Feature와 +dispatch 안의 일반 계획 파일 작성·할당된 실행에 반복 승인 gate를 추가하지 않는다. +권한 상승, 원격·파괴 작업, licensed reference solver 실행은 별도 명시적 승인이 필요하다. ## 2. 요구사항 탐색과 구체화 예를 들어 사용자가 다음 요구사항을 전달했다고 가정한다. +아래 Calculator 예시는 Harness Step 구조를 설명하기 위한 것으로 FESA 제품 요구사항이나 +새 기능 구현 지시가 아니다. + > CMake 기반 C++20 라이브러리에 `divide()` 함수를 추가하고, 0으로 나누면 예외를 > 발생시키며 GoogleTest 테스트를 작성한다. @@ -54,8 +64,10 @@ phase 파일을 생성한 다음 `scripts/execute.py`를 실행해야 구현 루 - `docs/PRD.md` - `docs/ARCHITECTURE.md` - `docs/ADR.md` +- Coordinator dispatch와 `docs//`의 승인된 선행 계약 +- 해당 Feature의 `FEATURED_AGENTS.md`, `PRD.md`, `ADR.md` - 관련 제품 코드와 테스트 -- `.harness/config.json` +- `.harness/config.json`이 있으면 그 설정, 없으면 preset·metadata·프로젝트 지침과 감지 기본값 이 탐색을 통해 다음 조건을 구체화한다. @@ -66,9 +78,9 @@ phase 파일을 생성한 다음 `scripts/execute.py`를 실행해야 구현 루 - 수정할 모듈과 범위 밖 항목 - 실행 가능한 Acceptance Criteria 명령 -요구사항에 결정되지 않은 부분이 있으면 구현 전에 사용자와 논의한다. 위 예에서는 -예외 타입, 정수 또는 부동소수점 연산 여부, public API와 ABI 변경 허용 여부가 이에 -해당한다. +필수 계약이 없으면 Coordinator를 통해 해당 owner로 반환한다. 예외 타입, 수치 타입, +public API·ABI처럼 승인된 의미를 바꾸는 선택은 critical decision 기준에 따라 사용자에게 +올린다. 이미 승인된 계약 안의 파일 배치나 테스트 분해는 담당자가 결정한다. ## 3. 요구사항을 Step으로 분해 @@ -80,7 +92,9 @@ phase 파일을 생성한 다음 `scripts/execute.py`를 실행해야 구현 루 - 하나의 모듈 또는 명확한 한 가지 책임만 다룬다. - 다른 대화 내용을 참조하지 않아도 실행할 수 있도록 자기완결적으로 작성한다. - 먼저 읽을 문서와 이전 Step의 관련 파일을 명시한다. +- Feature 관리 문서와 승인된 선행 계약의 정확한 경로·revision을 명시한다. - 클래스와 함수 시그니처 수준으로 작업 범위를 설명한다. +- 수정할 소스·테스트·보고서 파일과 수정하지 않을 upstream 파일을 명시한다. - 실제 실행 가능한 빌드·테스트 명령을 Acceptance Criteria로 사용한다. - 성공, 오류, 사용자 개입 필요 상태의 판정 기준을 적는다. - 범위 밖 기능과 기존 테스트 회귀를 명시적으로 금지한다. @@ -130,9 +144,10 @@ Step 1: 제품 코드를 구현해 테스트 통과 즉, 테스트가 구현보다 먼저 작성되는 순서는 지키되 각 Step은 최종적으로 green 상태여야 한다. -## 4. 사용자 승인 후 생성되는 파일 +## 4. 할당된 계획 산출물 -Step 초안을 사용자가 승인한 뒤에만 다음 파일을 생성한다. +Planning Agent는 Coordinator가 할당한 다음 파일을 생성한다. 기존 산출물을 재개할 때는 +덮어쓰지 않고 승인된 변경 범위만 갱신한다. Step 번호는 0부터 연속으로 둔다. ```text phases/ @@ -171,7 +186,8 @@ phases/ { "step": 0, "name": "division-api", - "status": "pending" + "status": "pending", + "owned_paths": ["include/calculator.hpp", "src/calculator.cpp", "tests/calculator_test.cpp"] } ] } @@ -179,13 +195,24 @@ phases/ 상태별 기록은 다음과 같이 나뉜다. +`owned_paths`는 Planning이 작성하는 정확한 저장소 상대 파일 목록이다. Step 본문의 소유 +파일과 일치해야 하며 source/header/test/build/report를 필요한 만큼 명시한다. 디렉터리, +glob, 절대 경로, 경로 이탈, Git 내부와 `phases/` 제어 경로는 허용하지 않는다. 연결된 +파일·디렉터리 경로도 지원하지 않는다. `[]`는 구현 파일 변경 없음이며 누락과 다르다. +기존 완료 Step은 필드 없이 읽을 수 있지만 모든 pending Step은 실행 전에 목록이 필요하다. +기존 phase를 일괄 변환하거나 Markdown에서 파일 권한을 자동 추출하지 않는다. + | 상태 | Codex가 기록 | Executor가 기록 | |---|---|---| | `completed` | `summary` | `completed_at` | | `error` | `error_message` | `failed_at` | | `blocked` | `blocked_reason` | `blocked_at` | -Executor는 task의 `created_at`과 Step의 `started_at`도 기록한다. `summary`는 다음 +Executor는 task의 `created_at`과 Step의 `started_at`, `attempt_history`도 기록한다. +History에는 단조 증가하는 `attempt`, `recorded_at`, `exit_code`(프로세스 미기동은 null), +`reported_status`, `reported_reason`, 로컬 `classification`·판정 `reason`과 `diagnostic`을 보존한다. +진단은 stderr 우선, 없으면 stdout의 마지막 4,000자이며 raw output 전체의 대체가 아니다. +Implementation은 `owned_paths`·history·timestamp를 수정할 수 없다. `summary`는 다음 독립 Codex 세션이 이전 Step의 핵심 산출물과 결정을 이해할 수 있도록 한 줄로 작성한다. @@ -206,13 +233,13 @@ Step은 독립 Codex 실행의 전체 작업 지시서이므로 이전 대화만 ## 5. Step Executor 시작 -계획 파일을 생성한 뒤 다음 명령으로 실행한다. +계획 파일이 준비되고 실행까지 할당된 경우 다음 명령으로 실행한다. ```powershell python scripts/execute.py add-division ``` -완료된 브랜치를 원격 저장소에 자동 push하려면 `--push`를 추가한다. +완료된 브랜치의 원격 push를 사용자가 명시적으로 승인한 경우에만 `--push`를 추가한다. ```powershell python scripts/execute.py add-division --push @@ -220,16 +247,19 @@ python scripts/execute.py add-division --push [Step Executor](../scripts/execute.py)는 시작할 때 다음 작업을 수행한다. -1. phase 디렉터리와 task index가 존재하는지 검사한다. -2. 이전 실행에서 `error` 또는 `blocked`로 끝난 Step이 있는지 검사한다. -3. `feat-{phase-name}` 브랜치를 생성하거나 checkout한다. -4. `AGENTS.md`와 `docs/*.md`를 guardrail로 읽는다. -5. task의 `created_at`이 없으면 기록한다. -6. 첫 번째 `pending` Step부터 순차 실행한다. +1. phase 경로와 top-level/phase index schema, Step 번호·이름·상태를 검사한다. +2. 현재 첫 미완료 Step이 `error` 또는 `blocked`이면 복구 결정 전까지 중단한다. +3. 모든 pending Step의 `owned_paths`, Implementation Agent profile, Git top-level, + sandbox 승인 조건과 clean 상태를 검사한다. + 기본 모드는 선택된 phase 제어 파일 allowlist 밖의 변경을 허용하지 않는다. +4. `feat-{phase-name}` 브랜치를 생성하거나 checkout한다. +5. `AGENTS.md`와 `docs/*.md`를 guardrail로 읽는다. +6. task/Step timestamp와 재개 상태를 정리하고 첫 번째 `pending` Step부터 순차 실행한다. `AGENTS.md`와 모든 `docs/*.md` 내용은 각 Codex 프롬프트에 직접 삽입된다. 따라서 -이 문서들은 참고 자료가 아니라 실제 실행 입력이다. 서로 충돌하거나 placeholder가 -남아 있으면 Codex도 그 모순을 입력으로 받는다. +이 문서들도 실행 입력에 포함된다. 역할·승인·상태 전이에서는 Agent 계약과 Harness skill의 +권한 경계를 따르며, 설치 예시를 Feature의 요구사항으로 사용하지 않는다. Feature 관리 +문서는 자동으로 로드되지 않으므로 dispatch와 Step의 필수 입력 경로에 명시해야 한다. ## 6. Step마다 독립 Codex 세션 실행 @@ -237,24 +267,29 @@ Executor는 각 Step을 다음 형태의 독립 프로세스로 실행한다. ```text codex exec + --disable unified_exec --json + --model + --config model_reasoning_effort="" --sandbox --dangerously-bypass-hook-trust --cd - ``` -기본값은 `workspace-write`다. `FESA_HARNESS_CODEX_SANDBOX` 환경 변수는 +기본값은 `workspace-write`다. `FEM_HARNESS_CODEX_SANDBOX` 환경 변수는 `workspace-write` 또는 `danger-full-access`만 허용한다. Windows native sandbox에서 MSVC compiler-id의 `cl.exe` 정지가 재현되고 동일 명령이 sandbox 밖에서 통과하는 -환경에서는, 사용자 승인을 받은 격리된 clean worktree 실행에 한해서 -`danger-full-access` fallback을 사용할 수 있다. 이 override는 hook trust 또는 hook -등록을 끄지 않으며 PreToolUse와 Stop 검증은 동일하게 실행된다. +환경에서는, 해당 실행 직전 사용자 승인을 받은 clean linked Git worktree 실행에 한해서 +`danger-full-access` fallback을 사용할 수 있다. Executor의 +`--approve-danger-full-access` flag도 필요하며 환경변수만으로 승인되지 않는다. +이 override는 등록된 hook을 비활성화하지 않는다. Codex에 전달하는 프롬프트는 다음 내용의 조합이다. ```text -AGENTS.md와 docs 문서 +Implementation Agent profile의 역할·수정 경계 ++ AGENTS.md와 docs 문서 + 이전에 완료된 Step의 summary + 이전 시도의 오류(재시도인 경우) + Executor 공통 작업 규칙 @@ -341,11 +376,19 @@ calculator.test.cpp 적용되지만 경로 기반 TDD 검사는 적용되지 않는다. 따라서 이 훅은 완전한 TDD 증명기가 아니라 테스트 우선 편집을 유도하는 guardrail이다. +PreToolUse는 허용된 편집 도구의 대상 경로를 `.harness/hook-state/`에 기록한다. +이 ignored 기록은 Stop의 문서 전용 판정 입력일 뿐 RED나 실제 편집 성공의 증거가 아니다. + ## 8. Codex 종료 전 Stop 검증 Codex가 Step 작업을 끝내고 응답을 종료하려 하면 -[Stop 훅](../scripts/hooks/stop_validation.py)이 실행된다. Stop 훅은 변경 파일만이 -아니라 발견된 C/C++ 프로젝트 전체를 빌드하고 테스트한다. +[Stop 훅](../scripts/hooks/stop_validation.py)이 실행된다. 유효한 동일 session/turn 기록에서 +편집 대상이 모두 저장소 내부 `.md`이면 build/test를 생략한다. 기록 누락·손상 또는 비문서 +편집이면 정상 검증 경로에서 발견된 C/C++ 프로젝트 전체를 빌드하고 테스트한다. + +Shell 쓰기는 완전히 추적되지 않으며, 편집 기록 없는 읽기 전용 요청도 자동 생략을 +보장하지 않는다. 사용자가 문서 작업의 실행을 제외하면 수동 Hook·build/test를 실행하지 +않고 정적 검토 범위와 미실행 항목을 보고한다. Hook state를 조작해 생략을 만들지 않는다. ### 8.1 저장소 루트와 재진입 방지 @@ -497,8 +540,10 @@ Harness는 별도 빌드 로그 파일을 생성하지 않는다. ### 11.1 Stop 검증 성공 -빌드와 테스트가 모두 성공하면 Stop 훅은 출력 없이 종료한다. Codex가 정상 종료하면 -Executor가 task index를 다시 읽는다. +빌드와 테스트가 모두 성공하거나 문서 전용 생략 조건을 충족하면 Stop 훅은 출력 없이 +종료한다. 생략은 build/test 통과 evidence가 아니다. Codex가 정상 종료하면 Executor는 +branch/HEAD, 제어 파일과 현재 Step 외 상태의 변경 여부를 검사하고 task index를 읽는다. +Codex 종료 코드가 0이 아니면 `completed` 기록만으로 성공 처리하지 않는다. Codex가 Step을 다음처럼 기록한 경우: @@ -507,6 +552,7 @@ Codex가 Step을 다음처럼 기록한 경우: "step": 0, "name": "division-api", "status": "completed", + "owned_paths": ["include/calculator.hpp", "src/calculator.cpp", "tests/calculator_test.cpp"], "summary": "divide API와 0 나누기 테스트를 추가함" } ``` @@ -520,32 +566,45 @@ Stop 훅은 Codex hook protocol에 따라 다음 형태의 응답을 출력한 ```json { - "continue": false, - "stopReason": "build failed ...", - "systemMessage": "build failed ..." + "decision": "block", + "reason": "build failed ..." } ``` -Codex 프로세스에 대한 훅 자체의 종료 코드는 0이지만 `continue: false`가 Codex의 -응답 종료를 막는다. Codex는 같은 세션에서 오류를 확인하고 수정을 계속한다. +훅 자체의 종료 코드는 0이지만 `decision: block`이 종료 차단 이유를 전달한다. +수정은 할당된 역할·Step·retry budget 안에서만 수행하며, Hook 실패가 상위 계약 변경이나 +무제한 반복 권한을 주지 않는다. ### 11.3 Executor 재시도 -Codex 프로세스가 끝났는데 Step 상태가 `completed` 또는 `blocked`가 아니면 Executor가 -새 Codex 세션으로 재시도한다. +Executor는 branch/HEAD·제어 파일·결과 형식과 변경 경로의 소유권을 먼저 검증한 뒤 +Step 보고와 프로세스 종료 코드를 함께 판정한다. + +| 결과 | 처리 | 자동 재시도 | +| --- | --- | --- | +| 인식된 sandbox helper 환경 오류(종료 코드 무관), CLI 미설치 | Step과 상위 phase `blocked`, 종료 2 | 없음 | +| 명시적 `blocked` 또는 `error`(종료 코드 무관) | 원래 사유 보존, 각각 종료 2 또는 1 | 없음 | +| `completed` + 종료 0 | 소유권·staged 경로 검증 후 커밋 | 없음 | +| `completed` + nonzero | `error`, 종료 1 | 없음 | +| 결과 미보고 + Executor wrapper의 실제 `TimeoutExpired` | transport timeout | 최초 포함 최대 2회 | +| 결과 미보고 + 종료 0, 원인 미분류 nonzero | `error`, 종료 1 | 없음 | + +종료 코드 124만으로 timeout을 추정하지 않는다. Wrapper가 실제 timeout을 관찰했을 때만 +raw output에 `transportFailure: "timeout"`을 기록한다. 제어·소유권 위반은 이 재시도보다 +우선하여 중단한다. 실패 결과를 `pending`으로 덮어쓰지 않고 원래 보고와 프로세스 진단을 +Executor 소유 history에 구분해 남긴다. ```text -첫 번째 시도 실패 +첫 번째 transport 시도 실패 → 오류를 다음 프롬프트에 삽입 → 두 번째 독립 Codex 실행 -→ 다시 실패하면 세 번째 독립 Codex 실행 -→ 세 번째도 실패하면 error 기록 후 종료 +→ 두 번째도 실패하면 error 기록 후 종료 ``` -즉, 실패 복구에는 두 층이 있다. - -1. Stop 훅이 같은 Codex 세션에서 수정하도록 요구한다. -2. 세션 자체가 성공하지 못하면 Executor가 새 세션으로 최대 3회 재시도한다. +Coordinator의 workflow correction은 별도 budget이다. 정규화된 +`feature_id|failing_gate|failure_classification|contract_revision`별로 Correction을 최대 +한 번, 그 뒤 Implementation rerun을 한 번만 허용한다. 계약·환경·물리 실패를 transport +재시도로 숨기거나 문구만 바꾸어 새 retry key를 만들지 않는다. ### 11.4 사용자 개입 필요 @@ -553,16 +612,21 @@ Codex 프로세스가 끝났는데 Step 상태가 `completed` 또는 `blocked` `blocked`로 기록한다. Executor는 `blocked_at`과 top-level 상태를 갱신하고 종료 코드 2로 중단한다. -재개하려면 원인을 해결하고 해당 Step을 `pending`으로 되돌린 뒤 -`blocked_reason`을 제거하고 다시 실행한다. `error`도 같은 방식으로 `pending`으로 -되돌리고 `error_message`를 제거한 뒤 재실행한다. +재개 전 Coordinator가 원인 분류, 실패 evidence, retry key와 허용된 다음 작업을 +`coordination.md`에 보존한다. Coordinator는 phase 파일을 직접 편집하지 않는다. +명시적으로 할당받은 Planning Agent가 Executor가 중단된 상태에서 phase index의 현재 +실패/차단 Step status만 `pending`으로 변경한다. 다른 Step, top-level 상태와 timestamp는 +변경하지 않는다. Executor가 재시작 시 오래된 outcome 필드·timestamp를 정리하되 +`attempt_history`는 유지한다. 기존 Step에 `owned_paths`가 없으면 승인 목록을 작성하는 +별도 Planning 할당이 필요하며 status 복구 권한만으로 목록을 추가·확대하지 않는다. +이는 할당된 파일 편집 절차이며 자동 복구 명령이나 추가 correction 허용이 아니다. ## 12. Git 커밋과 phase 완료 Step이 성공하면 제품 변경과 Harness metadata를 분리해 다음 형식으로 커밋한다. ```text -feat(add-division): step 0 — division-api +feat(add-division): step 0 - division-api chore(add-division): step 0 output ``` @@ -577,9 +641,21 @@ chore(add-division): step 0 output - 최종 metadata 커밋 - `--push` 사용 시 `origin/feat-{phase-name}`으로 push -커밋 과정은 `git add -A`를 사용한다. 실행 전에 작업 트리에 관련 없는 사용자 -변경사항이 남아 있으면 그 변경도 Step 커밋에 포함될 수 있다. 따라서 깨끗한 -worktree 또는 별도 Git worktree에서 실행하는 것이 안전하다. +Executor는 신뢰된 `owned_paths` snapshot으로 매 시도 후와 staging 직전의 변경 경로를 +검사한다. tracked/untracked, staged/unstaged, 생성·수정·삭제와 rename 양쪽 경로가 대상이다. +Windows 경로 구분자·대소문자 alias를 정규화하며 디렉터리·링크 대체도 다시 검사한다. +범위 밖 변경이 있으면 index를 정리하기 전에 중단하고 해당 경로와 사용자 파일을 보존한다. + +검사 후 검증된 경로 snapshot에 한정해 index만 정리하고 +`git --literal-pathspecs add -A -- <선택 경로>`를 사용한다. 저장소 전체 index를 reset하지 않는다. +Commit 직전에는 `git diff --cached --name-only --no-renames -z`의 실제 staged 경로도 +선택된 커밋 그룹과 대조한다. 구현 커밋에 phase metadata가 섞이는 것도 거부한다. +범위 위반 시 추가 stage/commit·재시도·다음 Step을 중단하며 파일을 되돌리거나 allowlist를 +자동 확대하지 않는다. staging 후 발견한 위반은 당시 index를 남기므로 재개 전 검토가 필요하다. + +동시 writer에 대한 원자적 격리는 보장하지 않으므로 실행 중 동시 편집을 피한다. 구현·metadata +커밋도 단일 transaction이 아니다. 구현 커밋 후 metadata 커밋이 실패하면 기존 커밋을 +되돌리지 않고 Step을 `error`로 기록해 Coordinator에 반환한다. ## 13. 요구사항 종류별 동작 @@ -589,7 +665,9 @@ worktree 또는 별도 Git worktree에서 실행하는 것이 안전하다. | 기존 C++ 구현 또는 header 수정 | 대응 테스트 파일 존재 여부 검사 | 전체 빌드·테스트 | | 테스트 파일 추가 | TDD 차단 없이 허용 | 모든 테스트가 성공해야 종료 | | `main.cpp` 수정 | TDD 대응 테스트 검사 면제 | 전체 빌드·테스트 | -| 문서, JSON, Python 수정 | C++ TDD 검사 없음 | C++ 프로젝트가 있으면 전체 검증 | +| 유효한 동일 session/turn 기록의 내부 `.md` 편집만 있음 | C++ TDD 검사 없이 경로 기록 | build/test 생략, 통과 evidence 아님 | +| JSON, Python 또는 다른 비문서 편집 | C++ TDD 검사 없음 | C++ 프로젝트가 있으면 전체 검증 | +| 편집 기록 없음·손상 또는 shell 쓰기 | 완전한 편집 추적을 보장하지 않음 | 문서 전용 자동 생략을 보장하지 않음 | | 위험한 Git 또는 삭제 명령 | 즉시 차단 | 도달하지 않음 | | C++ 파일은 있지만 build metadata 없음 | 편집은 허용될 수 있음 | orphan 프로젝트 오류 | | 직접 MSBuild인데 `testCommand` 없음 | 편집은 허용될 수 있음 | 설정 오류로 종료 차단 | @@ -597,18 +675,19 @@ worktree 또는 별도 Git worktree에서 실행하는 것이 안전하다. ## 14. 적용 전 준비사항 -이 저장소는 대상 C++ 프로젝트에 맞게 채워 사용하는 템플릿이다. 실행 전 다음을 -확인한다. +FESA는 실제 구조해석 솔버 프로젝트이며 Harness는 재사용 가능한 개발 운영 인프라다. +일반 Feature 실행 전에 다음을 확인한다. -1. `AGENTS.md`의 프로젝트명, toolset, C++ 표준, 테스트 프레임워크, CRITICAL 규칙을 - 실제 값으로 교체한다. -2. `docs/PRD.md`, `docs/ARCHITECTURE.md`, `docs/ADR.md`의 placeholder와 예시를 실제 - 프로젝트 정보로 교체한다. +1. `AGENTS.md`, 공통 PRD·ARCHITECTURE·ADR와 승인된 Feature 범위를 읽는다. +2. Feature 관리 문서·선행 계약·revision과 Planning/Implementation dispatch를 확인한다. + 일반 Feature 작업에서 프로젝트 공통 문서를 템플릿처럼 다시 작성하지 않는다. 3. 기본 자동 감지로 충분하지 않을 때만 `.harness/config.example.json`을 참고해 `.harness/config.json`을 만든다. -4. CMake 또는 MSBuild metadata와 테스트 실행 방법을 확인한다. -5. `phases/`가 없다면 요구사항 논의와 계획 승인을 거쳐 task 파일을 먼저 만든다. -6. Executor 실행 전에 Git working tree가 깨끗한지 확인한다. +4. FESA의 CMake/MSVC x64 Debug·CTest 명령과 dependency 경로를 확인한다. 다른 저장소에 + Harness를 적용할 때만 그 프로젝트의 승인된 adapter를 선택한다. +5. phase 파일이 없다면 Planning Agent가 할당된 파일을 작성한다. 실행 권한은 별도로 확인한다. +6. 시작 전 clean 범위와 실행 중 동시 편집 여부를 확인한다. 권한 상승은 clean linked + worktree, native sandbox 실패 증거와 해당 실행 직전 승인·flag가 모두 필요하다. 특히 `AGENTS.md`와 `docs/*.md`는 각 Codex 실행에 그대로 주입된다. C++ 프로젝트에서 TypeScript 예시나 미완성 placeholder가 남아 있으면 실제 작업 지시와 충돌할 수 있다. diff --git a/docs/PRD.md b/docs/PRD.md index 71b3964..bf2a08f 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,179 +1,148 @@ # PRD: FESA 구조해석 솔버 -## 목표 -FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 자체 유한요소 정식화로 구조해석을 -수행하고, step/frame 단위 결과를 input lexical sibling `_results.h5` HDF5로 저장하며, 기능이 지정한 Abaqus -reference CSV 수치와 비교 가능한 C++17/MSVC 솔버를 제공한다. Abaqus의 요소 알고리즘, -적분법, stabilization 또는 내부 결과 생성 절차를 재현하는 것은 목표가 아니다. +## 제품 정의 -이 프로젝트의 성공 기준은 단순 실행 성공이 아니다. 기능 구현은 요구조건, 정식화, -I/O 계약, C++ build/test와 기능별 blocking reference comparison을 통과해야 완료된다. -Physics sanity와 release readiness는 구현 완료 뒤 별도 배포 판단을 제공하며 Abaqus 내부 -동작 동등성을 요구하지 않는다. +FESA는 유한요소법 기반 구조해석 솔버를 단계적으로 구축하는 프로젝트이다. 물리적으로 +설명 가능하고 재현 가능한 해석 결과를 제공하며, 새로운 기능을 요구사항부터 독립 검증까지 +추적 가능한 단위로 확장하는 것을 목표로 한다. -## 사용자 -- Solver developer: C++17/MSVC/CMake/CTest 환경에서 요소, 재료, 해석 절차, solver backend를 구현한다. -- Verification reviewer: reference artifact, tolerance, physics sanity, release readiness를 검토한다. -- Analyst preparing Abaqus-compatible input subsets: FESA가 지원하는 제한된 `.inp` subset에 맞춰 입력 모델을 준비한다. -- Codex agent workflow operator: project-local agent와 skill을 사용해 요구조건부터 release까지의 gate를 운영한다. +이 문서는 특정 요소, 재료, 하중 또는 해석 절차의 명세가 아니라 모든 Feature에 적용되는 +제품 기준이다. 개별 지원 범위, 입력 옵션, 수식, 수치 기준과 검증 사례는 +`docs//`의 승인 계약으로 정의한다. 특정 기능의 코드가 존재한다는 사실과 +그 기능의 검증 완료 또는 현재 release readiness는 서로 다른 상태다. -## 핵심 기능 -1. Abaqus `.inp` keyword subset parser와 내부 `Domain` semantic model 생성 -2. `AnalysisModel`, `DofManager`, `AnalysisState` 기반의 step별 equation system 구성 -3. 선형 정적 해석을 시작점으로 하는 `Analysis` procedure 계층 -4. Stable source identity를 보존하는 concrete V0 node, B33 element, material, section, boundary, load record -5. sparse matrix pattern 생성, 전역 행렬/벡터 조립, 제약조건 적용 -6. `LinearSolver`, `ParallelFor`, `ResultsWriter` interface를 통한 MKL PARDISO, oneTBB, HDF5 backend 격리 -7. HDF5 기반 versioned step/frame field-result 저장과 향후 history-output 확장 경계 -8. FESA HDF5 rows와 `reference//` 아래 Abaqus reference CSV rows의 직접 비교 -9. CMake/MSVC/x64/Debug, CTest 기반 개발 검증 +현재 선택된 기술 스택과 외부 형식은 [프로젝트 지침](../AGENTS.md) 및 승인된 계약을 +따른다. 공통 문서의 일반화는 기존 지원 범위, 파일 형식, 수치 정책 또는 호환성을 변경하는 +승인이 아니다. -Element/material 계층의 일반적인 runtime polymorphism, iterative solver, history output은 장기 확장 방향이다. 현재 제품 계약으로 간주하지 않으며, 실제 사용 사례와 성능·수명 요구가 승인되기 전에 추상 base class를 미리 추가하지 않는다. +## 대상 사용자 -## 사용자 관점 제품 흐름 +- Solver 개발자: 요소, 재료, 하중, 제약, 해석 절차와 수치 backend를 구현한다. +- 해석 사용자: 문서화된 입력 범위로 모델을 구성하고 결과와 진단을 해석한다. +- 검증 담당자: 수학적 일관성, 물리적 타당성, reference 비교와 회귀 evidence를 검토한다. +- Maintainer와 workflow 운영자: 요구사항, 결정, 구현과 검증의 추적성 및 변경 범위를 관리한다. -1. 사용자는 승인된 Abaqus `.inp` subset으로 모델과 단일 analysis step을 기술한다. -2. FESA는 syntax를 읽은 뒤 semantic mapping과 model validation을 수행한다. 지원하지 않는 모델 의미는 구조화된 diagnostic과 안정된 exit code로 거부한다. -3. 해석 procedure는 active model view, DOF/equation map과 sparse system을 만들고, essential constraint elimination을 적용해 선형계를 factorize하고 substitution한다. -4. FESA는 full displacement를 복구하고 equilibrium residual/reaction과 element 결과를 계산한다. -5. 성공 시 output request와 무관한 mandatory 결과, metadata와 diagnostic을 versioned lexical sibling `_results.h5`에 기록한다. 실패 시 불완전한 최종 결과 파일을 정상 결과처럼 남기지 않는다. -6. 검증 workflow는 기능이 지정한 HDF5 quantity를 기존 Abaqus CSV의 source identity와 - component에 대응시켜 tolerance를 판정하고, 별도로 equilibrium과 물리적 타당성을 - 검토한다. Reference bundle naming이나 provenance는 수치 비교 gate가 아니다. +## 제품 목표 -Parser keyword, element kernel, solver backend 또는 output dataset 중 하나만 추가된 상태는 end-to-end 제품 기능이 아니다. 같은 기능의 입력 의미부터 외부 결과와 검증까지 연결되어야 한다. +1. 해석의 물리·수치 의미를 입력부터 결과까지 보존한다. +2. 작은 Feature를 end-to-end로 완성한 뒤 검증된 기반 위에서 지원 범위를 확장한다. +3. 모델 의미, 계산 상태, 수치 backend와 저장 형식의 책임을 구분한다. +4. 같은 입력과 승인된 실행 조건에서 source identity, 조립·결과·진단 순서를 재현한다. +5. 잘못되거나 지원하지 않는 모델 의미를 조용히 근사하지 않고 명확히 진단한다. +6. 요구사항, 설계 결정, 구현, 테스트와 검증 결과를 같은 Feature와 contract revision으로 연결한다. -## V0 범위 -- 선형 정적 해석 파이프라인 구현 -- 승인된 첫 end-to-end 기능: 2절점 3D Euler–Bernoulli beam, Abaqus `TYPE=B33` -- 입력 파일당 하나의 `*STEP, *STATIC` -- 절점당 자유도 `[UX, UY, UZ, URX, URY, URZ]` -- 축, 두 방향 굽힘, Saint-Venant 비틀림 및 선형 등방 탄성 -- 최소 Abaqus keyword subset: - - `*HEADING` - - `*PART`, `*END PART` - - `*NODE`, `*ELEMENT, TYPE=B33` - - `*NSET`, `*ELSET`, including `GENERATE` - - `*MATERIAL` - - `*ELASTIC` - - `*BEAM GENERAL SECTION, SECTION=GENERAL` - - `*SECTION POINTS` - - `*ASSEMBLY`, `*END ASSEMBLY` - - `*INSTANCE`, `*END INSTANCE` - - `*BOUNDARY` - - `*CLOAD` - - `*STEP`, `*STATIC`, `*END STEP` -- `*PREPRINT`, `*RESTART`, `*TRANSVERSE SHEAR STIFFNESS`, `*OUTPUT, FIELD`, - `*OUTPUT, HISTORY`, `*NODE OUTPUT`, `*ELEMENT OUTPUT`, `*CONTACT OUTPUT`과 그에 속한 - 미지원 variable data는 warning 후 no-op 처리 -- 같은 part의 여러 identity instance와 stable source label mapping -- nonzero prescribed displacement를 포함한 free/constrained partition -- formulation의 constant local line-load equivalent nodal vector kernel. V0 parser와 CLI는 - `*DLOAD`를 지원하거나 이 kernel을 호출하지 않음 -- deterministic COO-to-CSR stiffness assembly -- MKL PARDISO 기반 sparse direct solver -- stiffness factorization과 load-vector substitution 분리 -- displacements, reactions, equilibrium end actions, section resultants, generalized results, - axial `S11` output -- HDF5 result schema v0 -- `fesa.exe ` CLI with lexical sibling result/log names -- 승인된 `reference/cantilever beam/` B33 결과와 displacement, reaction, section resultant - comparison; beam stress reference comparison은 N/A +Harness와 agent·skill은 이 목표를 달성하기 위한 개발 운영 인프라다. 운영 스크립트나 +문서 체계 자체를 구조해석 솔버의 제품 기능으로 간주하지 않는다. -## 승인된 Nonlinear Static 확장 +## 문서와 계약의 역할 -- `*STATIC, NLGEOM=YES` 단일 step은 parse/map을 정확히 한 번 수행한 뒤 linear static과 - 분리된 full-Newton procedure로 dispatch한다. Nonlinear path는 별도 mutable state, - unsymmetric tangent solver, iteration log writer와 schema-v1 HDF5 writer를 사용한다. -- CLI는 정확히 하나의 ``만 받고 `_results.h5`와 `_log.txt`를 같은 - lexical sibling directory에 유도한다. `--output`은 지원하지 않는다. -- Nonlinear HDF5 schema v1은 committed accepted frame만 기록한다. factor-zero는 metadata - baseline이며 accepted frame이 아니고, successful factor-one row는 unique - `load_factor=1.0` accepted frame이다. -- Factor-one `/global/equilibrium`은 global origin에서 current `X+u` arm과 spatial physical - moment를 사용한 applied-plus-constrained-reaction balance error다. Accepted free residual과 - constrained-reaction resultant는 그 dataset의 대체 quantity가 아니다. -- `shellNL-meshfine`는 primary nonlinear case, `shellNL`은 companion, `shell`은 linear - regression control이다. `U1/U2/U3`는 blocking, `UR1/UR2/UR3`는 warning-only이며 local - `RF/RM`은 review-only (`RM3` 및 stress 제외)로 비교한다. +| 문서 | 소유하는 내용 | +| --- | --- | +| 이 PRD | 제품 목적, 공통 요구사항, 품질 속성과 완료 기준 | +| [ARCHITECTURE](ARCHITECTURE.md) | 공통 책임, 의존성, 수명과 확장 경계 | +| [ADR](ADR.md) | 프로젝트 전반의 설계 결정과 변경 이력 | +| Feature `PRD.md` | 해당 요구의 목적·범위·제외·acceptance 요약과 상세 요구사항 링크 | +| Feature `requirements.md` | Requirement Agent가 소유하는 검증 가능한 요구사항과 verification matrix | +| Feature `ADR.md` | 해당 Feature의 결정·이유·영향·owner와 승인 근거 | +| Feature 전문 산출물 | 연구, 정식화, 수치/reference 검토, I/O, 구현·검증, 물리 검토와 release evidence | -## 신규 기능의 제품 완료 정의 +Feature 문서의 생성·재개, Coordinator 관리 문서와 상세 문서 분리는 +[FEATURE_DOCUMENTATION](FEATURE_DOCUMENTATION.md)을 따른다. Feature PRD가 +`requirements.md`와 별도의 요구사항 원본을 만들거나 공통 기준을 묵시적으로 덮어쓰지 않는다. +공통 기준 자체의 변경은 별도로 승인된 프로젝트 문서 작업으로 다룬다. -새로운 element, load, constraint, material, analysis procedure 또는 output quantity는 다음 조건을 모두 만족할 때 FESA 제품 기능으로 취급한다. +## 공통 기능 요구사항 -1. 지원 범위와 제외 범위, 실패 category, solver output의 units/coordinates/identity, - blocking reference quantity와 tolerance가 요구조건과 I/O 계약에 명시되어 있다. -2. 필요한 FEM 정식화와 recovery/sign convention이 검토되었고, 수치 위험과 검증 문제를 numerical review가 다룬다. -3. Syntax와 semantic mapping이 내부 모델에 안정된 source identity로 연결되며 잘못된 입력을 fail-closed로 거부한다. Kernel만 제공하는 기능은 parser/CLI 지원 여부를 별도로 표시한다. -4. DOF, sparse pattern, assembly, constraint, solver lifecycle과 mutable state ownership이 기존 아키텍처에 연결되거나 승인된 새 procedure 경계를 가진다. -5. Mandatory HDF5 schema와 diagnostic, CLI 실패 의미가 정의되고 failure path가 기존 state 또는 최종 output을 오염시키지 않는다. -6. Unit test가 local 수식과 validation을, integration test가 orchestration과 외부 계약을, - required reference comparison이 기능별 blocking quantity의 외부 수치 acceptance를 - 각각 증명한다. Reference가 N/A이면 그 이유와 대체 evidence를 명시한다. +아래 `FESA-PRD-GEN-*`는 공통 원칙을 위한 추적 ID다. 기존 `FESA-PRD-001`부터 +`FESA-PRD-019`까지의 의미를 바꾸어 재사용하지 않으며, 그 원문과 적용 범위는 +[개정 전 PRD](archive/pre-generalization/PRD.md)에 보존한다. Feature의 상세 요구사항은 +별도 `FEM-REQ--###` ID와 해당 공통 기준을 연결한다. -## 기능 요구조건 -| ID | 요구조건 | Acceptance Criteria | Verification Method | -| --- | --- | --- | --- | -| FESA-PRD-001 | FESA는 Abaqus `.inp` full compatibility가 아니라 승인된 keyword subset만 지원해야 한다. | 지원/미지원 keyword가 문서화되고, 미지원 keyword는 구조화된 diagnostic을 남긴다. | I/O contract review, parser unit test | -| FESA-PRD-002 | FESA는 입력 모델을 `Domain`으로 변환해야 한다. | nodes, elements, materials, properties, sets, loads, boundary conditions, step definitions가 semantic model에 보존된다. | parser integration test | -| FESA-PRD-003 | FESA는 현재 step의 실행 view를 `AnalysisModel`로 구성해야 한다. | active elements, loads, boundary conditions, properties/materials가 Domain 복사 없이 참조 또는 id view로 연결된다. | analysis model unit test | -| FESA-PRD-004 | FESA는 equation numbering과 constraint/free mapping을 `DofManager`에 집중해야 한다. | Node/Element 내부에 equation id를 분산 저장하지 않는다. | code review, DofManager unit test | -| FESA-PRD-005 | FESA는 해석 중 변하는 물리량을 `AnalysisState`에 저장해야 한다. | V0 displacement, external/internal force, residual, constrained reaction, step/frame identity와 element recovery rows가 출력에 연결되고 velocity, acceleration, temperature, iteration history는 할당하지 않는다. | state unit test, integration test | -| FESA-PRD-006 | FESA는 solver 결과를 derived HDF5 authoritative output `_results.h5`로 저장해야 한다. | V0 step/frame field results, metadata, diagnostics가 schema version과 함께 저장된다. | HDF5 schema test | -| FESA-PRD-007 | FESA는 기능이 지정한 Abaqus reference CSV quantity와 비교 가능한 deterministic source-row/component mapping을 제공해야 한다. | Required source IDs/components는 일대일 대응되고 missing/extra/duplicate/nonfinite row는 tolerance 전에 실패한다. Canonical naming, README, metadata, provenance 또는 CSV schema version은 요구하지 않는다. | reference comparison report | -| FESA-PRD-008 | FESA의 production C++ 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현해야 한다. | 관련 C++ test file, RED 실패와 후속 GREEN 성공 증거가 있고 Stop의 전체 MSVC build/test가 통과한다. | implementation report, Hook guardrail, CTest | -| FESA-PRD-009 | FESA는 외부 라이브러리 API를 solver core에 직접 노출하지 않아야 한다. | MKL, TBB, HDF5 의존은 adapter module에 제한된다. | architecture review, dependency review | -| FESA-PRD-010 | FESA 기능 구현 완료는 build/test와 기능별 blocking reference comparison 통과를 요구해야 한다. | Required CTest가 통과하고 선언된 blocking quantity가 승인 tolerance 안에 있다. Physics sanity와 release readiness는 별도 후속 gate다. | build/test report, reference verification report | -| FESA-PRD-011 | V0는 입력 파일당 하나의 linear static step과 Abaqus B33 3D Euler beam만 해석해야 한다. | 단일 B33 model은 해석되고, B31과 두 번째 step은 구조화된 unsupported diagnostic으로 거부된다. | parser/semantic unit test, CLI integration test | -| FESA-PRD-012 | V0는 Part/Assembly/Instance wrapper와 source identity를 보존해야 한다. | 여러 identity instance의 node/element/set label이 stable internal ID로 deterministic하게 매핑되고 transform 또는 nested assembly는 거부된다. | semantic mapping unit test | -| FESA-PRD-013 | Output-request allowlist는 해석 의미를 변경하지 않아야 한다. | 승인 keyword와 variable data는 warning 후 no-op이고, allowlist 밖 model-affecting keyword는 오류이며 기본 결과는 output request와 무관하게 생성된다. | parser diagnostic test, HDF5 integration test | -| FESA-PRD-014 | 선형 정적 pipeline은 stiffness factorization과 load substitution을 분리해야 한다. | `Kff` factorization이 load vector assembly보다 먼저 수행되고, `rhs=Ff-Kfc*dc` substitution으로 full displacement를 복구한다. | orchestration test, solver-adapter test | -| FESA-PRD-015 | FESA는 V0 결과와 diagnostic을 안정된 외부 계약으로 출력해야 한다. | CLI가 `0=success`, `2=usage`, `3=input`, `4=model`, `5=solver`, `6=HDF5` exit code를 사용하고 diagnostic field가 계약과 일치하며 HDF5가 displacement, reaction, end force, section resultant, generalized strain/resultant, axial `S11`을 포함하고 실패 시 불완전한 최종 파일을 남기지 않는다. | CLI integration test, HDF5 schema/atomicity test | -| FESA-PRD-016 | Dense와 sparse math storage 및 backend 경계를 분리해야 한다. | `Vector`는 contiguous, `Matrix`는 row-major contiguous storage와 MKL CBLAS를 사용하고 `SparseMatrix`는 별도 0-based CSR 타입이며 MKL 타입이 public core API에 노출되지 않는다. | math unit test, dependency review | -| FESA-PRD-017 | 모든 외부 reference comparison은 승인된 공통 family-scale tolerance를 사용해야 한다. | 각 matched row가 near-zero 대체 조건 또는 상대오차 `0.05`를 통과하고 각 family의 scale-relative RMS가 `0.01` 이하이며 missing/extra/duplicate/nonfinite row는 comparison 전에 실패한다. | reference comparison unit/integration test, verification report | -| FESA-PRD-018 | 승인된 B33 reference artifact는 현재 경로의 read-only baseline으로 유지해야 한다. | `reference/cantilever beam/` 파일을 rename, rewrite 또는 보정하지 않고 기능이 선언한 exact path에서 읽는다. | artifact inventory, Git diff review | -| FESA-PRD-019 | B33 beam section과 local axis를 Abaqus 의미에 맞게 매핑해야 한다. | `n1 -> local y`, `t x n1 -> local z`, `Iy=I11`, `Iz=I22`, `I12=0`을 적용하고 nonpositive property, zero-length element, tangent-parallel guide vector를 구조화된 model diagnostic으로 거부한다. | section-mapping unit test, element geometry test | +| ID | 요구사항 | 확인할 evidence | +| --- | --- | --- | +| FESA-PRD-GEN-001 | 지원·미지원 범위와 관찰 가능한 동작을 Feature 계약으로 명시해야 한다. | 범위·제외·입력/출력 계약 및 양성·음성 검증 | +| FESA-PRD-GEN-002 | 입력을 일관된 semantic model로 변환하고 잘못된 참조·중복·비유한 값·미지원 조합을 진단해야 한다. | Mapping·validation 계약과 실패 경로 테스트 | +| FESA-PRD-GEN-003 | Source identity, internal index, DOF, equation과 result identity를 구분하고 필요한 대응 관계를 보존해야 한다. | Identity mapping과 재정렬·누락·중복 검사 | +| FESA-PRD-GEN-004 | 모델 정의, 실행 view, equation 구성과 mutable state의 소유권·수명을 분리해야 한다. | Architecture·lifetime 검토와 관련 테스트 | +| FESA-PRD-GEN-005 | 요소·재료·하중·제약의 수학적 계약을 검토한 뒤 local 계산과 전역 해석을 연결해야 한다. | 정식화, Numerical Review, local·integration evidence | +| FESA-PRD-GEN-006 | 해석 절차마다 필요한 state, 해법, 종료 조건, commit과 recovery 의미를 정의해야 한다. | Procedure 계약과 성공·실패 lifecycle 검증 | +| FESA-PRD-GEN-007 | 결과의 물리량, location, units, coordinates, component, entity와 step/frame 의미를 식별할 수 있어야 한다. | I/O·result schema 계약과 mapping 검증 | +| FESA-PRD-GEN-008 | 실패한 candidate가 유효한 state나 최종 결과를 부분적으로 오염시키지 않도록 계약된 보존 범위를 지켜야 한다. | State commit·복구·출력 실패 검증 | +| FESA-PRD-GEN-009 | 외부 형식과 vendor/OS API를 adapter 뒤에 격리하고 solver core에 노출하지 않아야 한다. | Public dependency와 adapter 경계 검토 | +| FESA-PRD-GEN-010 | Production 동작 변경은 관련 테스트의 기대 실패를 관찰한 뒤 구현하고 지정된 검증 evidence를 남겨야 한다. | RED/GREEN, focused/full 검증 명령과 실제 결과 | +| FESA-PRD-GEN-011 | 기능에 필요한 독립 검증과 reference 비교를 수행하고 승인된 identity·수치 정책을 유지해야 한다. | 검증 matrix, 사전검사, reference 또는 명시적 N/A와 대체 근거 | +| FESA-PRD-GEN-012 | 기능 완료는 같은 계약 revision의 필수 evidence와 미해결 항목을 추적하여 판정해야 한다. | 요구사항부터 물리 검토·release readiness까지의 gate 기록 | -## 비기능 요구조건 -- MSVC x64 Debug 환경에서 configure, build, CTest를 검증한다. -- reference test 결과는 deterministic해야 한다. -- HDF5 schema는 versioned contract로 관리한다. -- 모든 외부 reference comparison은 동일 logical quantity와 unit dimension의 component - family별 Abaqus-only maximum scale을 사용한다. Near-zero ratio는 `0.01`, 일반 행 - 상대오차는 `0.05`, family scale-relative RMS는 `0.01`이며 독립 absolute-error gate는 - 사용하지 않는다. -- B33 section resultant는 source element label과 endpoint node label로 직접 대응한다. - MITC4 `U1/U2/U3`만 blocking이고 `UR1/UR2/UR3` 초과는 warning-only다. -- parser, solver, HDF5 writer는 실패 원인을 구조화된 diagnostic으로 보고한다. -- oneMKL, oneTBB, HDF5는 CMake에서 명시 탐지하고 실패 원인을 분류한다. -- 대규모 모델 성능 최적화보다 V0의 명확성, 테스트 가능성, 검증 traceability를 우선한다. +요구사항 단계는 acceptance intent와 필요한 물리적 관찰을 정한다. Numerical Review는 +기존 승인 정책 안에서 논리적 비교량·identity/component·판정 구분을 구체화하고, I/O는 이를 +외부 syntax와 결과 schema에 대응시킨다. 구현 담당자가 누락된 상위 계약을 임의로 정하지 않는다. -## Acceptance Gates -1. Requirements approved: 기능 범위, 제외 범위, 입력, 출력, tolerance, 검증 물리량이 정의되어 있다. -2. Research evidence complete: 정식화와 benchmark 근거가 신뢰도와 한계와 함께 정리되어 있다. -3. Formulation reviewed: 약형, shape function, B matrix, constitutive contract, 수치적분, output recovery가 검토되어 있다. -4. I/O contract approved: Abaqus keyword subset, internal model mapping, HDF5 result contract, - required reference quantity의 최소 source-ID/component matching이 승인되어 있다. -5. Tests fail before implementation: C++/integration/reference test를 제품 코드보다 먼저 작성하고 같은 Step 안에서 RED 실패와 후속 GREEN 성공을 확인한다. -6. CMake/CTest pass: MSVC/x64/Debug 기준 configure, build, test가 통과한다. -7. Reference comparison pass: 기능이 blocking으로 선언한 FESA `_results.h5` quantity가 - 선언된 Abaqus CSV와 source identity/component별로 대응되고 승인된 tolerance 안에 있다. -8. Physics sanity pass: equilibrium, reaction consistency, displacement direction, symmetry, section-force consistency와 normalized residual이 검토되어 있다. -9. Release readiness pass: acceptance traceability, known limitations, release notes draft가 준비되어 있다. +## 품질 속성 -## 제외 사항 -- Abaqus full parser 호환 -- Abaqus B31/Timoshenko beam -- 다중 analysis step과 step 간 load/BC propagation -- instance translation/rotation, nested assembly 및 dependent/independent mesh semantics -- `I12 != 0`, taper, offset, release, curved beam, warping -- Abaqus `*DLOAD` 입력과 Domain distributed-load object -- 승인된 fixed-load-factor nonlinear-static 범위를 벗어난 기하·재료비선형, dynamics, - contact, thermal effects -- transverse shear stress와 torsional shear stress recovery -- beam stress에 대한 Abaqus reference comparison -- Abaqus, Nastran 또는 reference solver 직접 실행 자동화 -- Agent가 Abaqus reference CSV 파일을 임의 생성 또는 수정하는 작업 -- GUI 또는 postprocessor -- Visual Studio `.sln`/`.vcxproj` 전용 MSBuild workflow -- plasticity와 shell end-to-end 구현 +| 속성 | 공통 기준 | +| --- | --- | +| Correctness | 실행 성공이나 유한한 결과만으로 판단하지 않고 수학적 계약과 기능별 검증으로 설명한다. | +| Determinism | 승인된 실행 조건에서 identity, numbering, reduction, 결과와 diagnostic 순서를 재현한다. | +| Traceability | 요구사항·결정·코드·테스트·artifact·verdict를 Feature ID와 revision으로 연결한다. | +| Extensibility | 실제 사용 사례에 필요한 최소 경계를 추가하며, 지원하지 않는 physics나 미래 state를 선할당하지 않는다. | +| Diagnosability | 실패 단계와 관련 source/entity, 원인과 재개에 필요한 정보를 제공한다. | +| Safety | 미지원 의미는 fail-closed로 처리하고, reference와 관련 없는 사용자 변경을 보존한다. | +| Maintainability | 사람이 계약과 수식을 대조할 수 있는 단순한 코드와 중복 없는 문서를 유지한다. | +| Performance | 정확성·재현성을 보존하고 측정된 병목과 승인된 요구를 근거로 최적화한다. | + +특정 모델 크기, 성능 목표와 환경별 보장은 해당 요구와 evidence가 있을 때 Feature 계약으로 +정한다. 수치 검증 기준은 적용되는 공통 정책과 승인된 Feature 계약을 따르며 외부 reference +기준을 기능마다 임의로 정하지 않는다. 이 표는 검증되지 않은 성능이나 모든 platform의 +동등성을 약속하지 않는다. + +## 검증과 완료 기준 + +검증은 서로 다른 오류를 확인하는 계층으로 구성한다. Numerical Review가 Feature에 맞는 +unit, analytical/benchmark, integration, convergence, invariant 및 외부 reference coverage를 +정의한다. 한 계층의 성공이 다른 필수 계층을 대신하지 않는다. + +- 외부 reference 비교는 identity와 component를 대응시키고 missing, extra, duplicate, + nonfinite 값을 거부한 뒤 [TOLERANCE](TOLERANCE.md)를 적용한다. +- 물리 평형, 수렴과 정식화 검증의 기준을 외부 reference tolerance와 혼용하지 않는다. +- Reference artifact는 read-only로 취급하며 구현 결과에 맞추어 생성·보정하지 않는다. +- 비교가 N/A인 경우 이유와 계약이 요구하는 대체 evidence를 명시한다. +- 실패·미실행·stale evidence를 통과로 표시하지 않는다. 과거 통과 기록을 현재 검증으로 + 소급 적용하지 않는다. + +정규 Feature workflow는 다음과 같다. + +`requirements → research → formulation → numerical-review → io-definition → +implementation-planning → implementation → physics-evaluation → release-readiness` + +구현 gate는 TDD, 지정된 전체 build/test와 필수 reference evidence가 갖추어졌을 때 +통과한다. Feature closure에는 그 뒤의 독립 물리 검토와 release-readiness 감사도 필요하다. +Release readiness는 내부 완료 권고이며 외부 publish, push, tag, deploy 또는 package 권한이 아니다. + +Owner, 공통 handoff, outcome, stale 처리와 유한 correction은 +[SOLVER_AGENT_DESIGN](SOLVER_AGENT_DESIGN.md)을 따른다. Skill의 적용 경계는 +[SOLVER_SKILL_DESIGN](SOLVER_SKILL_DESIGN.md), 실행 절차는 각 현재 `SKILL.md`를 따른다. +문서만 수정하는 작업을 solver Feature 전체 workflow로 확대하지 않는다. + +## 단계적 확장과 비목표 + +새 Feature는 입력 의미부터 결과와 검증까지 연결되는 작은 단위로 정의한다. Kernel, +parser 또는 dataset 하나가 존재한다는 이유만으로 제품 노출을 선언하지 않는다. +공통 문서에 구현 항목별 목록이나 진행 상태를 누적하지 않고 해당 Feature 문서에서 관리한다. + +다음은 별도 승인 없이 목표로 삼지 않는다. + +- 특정 외부 solver의 전체 호환성이나 비공개 알고리즘·내부 상태·결과 생성 절차의 동등성 +- 아직 요구되거나 검증되지 않은 요소·재료·해석 절차·입출력 형식의 지원 +- GUI, 모델러, mesher 또는 범용 전후처리기로의 제품 범위 확대 +- 외부 solver 실행, reference 갱신, 원격 게시 또는 파괴적 작업의 자동 승인 + +외부 제품의 공개 입력 의미, 출력 정의와 검증 사례는 승인된 범위의 reference로 사용할 수 +있다. 수치 결과의 exact equality는 허용되지만 내부 동작 동등성을 증명하지 않는다. + +## 개정 이력의 보존 + +이 개정은 프로젝트 공통 기준과 구현 항목별 계약의 문서 경계를 정리한다. 기존 기능의 +요구사항을 폐기하거나 수치 acceptance를 변경하지 않는다. 기능별 설명과 기존 ID의 원문은 +[개정 전 PRD](archive/pre-generalization/PRD.md)에 보존하며, 향후 Feature PRD로 이관할 때 +원래 범위·revision·근거를 함께 연결한다. 보존본의 오래된 지원·검증 상태보다 해당 Feature의 +승인된 최신 계약과 evidence를 확인한다. diff --git a/docs/archive/pre-generalization/ADR.md b/docs/archive/pre-generalization/ADR.md new file mode 100644 index 0000000..4ffd9b1 --- /dev/null +++ b/docs/archive/pre-generalization/ADR.md @@ -0,0 +1,375 @@ +# Architecture Decision Records + +## 철학 +FESA의 architecture decision은 solver correctness, verification traceability, implementation testability를 우선한다. Harness는 제품이 아니라 C++/MSVC 기반 FEM 구조해석 솔버 개발을 통제하는 운영 인프라이다. + +--- + +### ADR-001: FESA는 구조해석 솔버 프로젝트이고 Harness는 운영 인프라로 둔다 +**결정**: 저장소의 주 목적은 유한요소법 기반 구조해석 솔버 개발이다. Harness는 승인된 Step 계획, 독립 세션 실행, PreToolUse guardrail, Stop MSVC build/test 검증을 제공하는 보조 계층으로 유지한다. + +**이유**: 기존 문서가 Harness 중심이면 agent가 solver architecture, FEM verification, Abaqus/HDF5 계약보다 운영 스크립트에 과도하게 맞춰 행동한다. + +**트레이드오프**: Harness 문서의 비중은 낮아지지만, `docs/HARNESS_WORKFLOW.md`의 실행 계약과 `.codex/hooks.json`의 검증 정책은 계속 필수 운영 규칙으로 유지한다. + +### ADR-002: C++17/MSVC/CMake/CTest를 기본 구현 환경으로 둔다 +**결정**: 기본 solver 구현과 validation은 C++17 이상, Visual Studio 17 2022 또는 +Visual Studio 18 2026 generator, x64 platform, Debug config, CMake, CTest로 수행한다. +`.harness/config.json`이 없으면 CMake가 설치된 두 승인 generator 중 하나를 자동 +선택할 수 있으며, build/test evidence에는 실제 generator와 compiler version을 기록한다. + +**이유**: FESA의 목표 환경은 Windows/MSVC 기반 C++이다. CMake/CTest는 solver source tree가 추가되거나 확장될 때 가장 일관된 build/test entry point다. VS17과 VS18을 +승인하면 현재 지원되는 MSVC 개발 환경에서 같은 C++17/x64/Debug 계약을 검증할 수 있다. + +**트레이드오프**: FESA solver source는 CMake/CTest를 기본으로 유지한다. Generator별 +compiler 차이는 각 build/test report에 명시하고, 두 generator 중 하나에서만 관찰되는 +실패를 다른 generator의 성공으로 숨기지 않는다. Harness 자체는 `.harness/config.json`에 +solution과 test command를 명시한 직접 MSBuild 프로젝트도 검증할 수 있지만, 이는 FESA +제품이 solution-only workflow를 지원한다는 의미가 아니다. + +### ADR-003: Abaqus `.inp` full compatibility가 아니라 기능별 keyword subset을 지원한다 +**결정**: FESA parser는 Abaqus keyword/data/comment line 규칙을 따르되, 기능별로 승인된 keyword subset만 지원한다. 미지원 keyword는 명확한 diagnostic을 남긴다. + +**이유**: full Abaqus compatibility는 초기 solver 범위와 검증 비용을 과도하게 키운다. 기능별 subset은 요구조건, I/O contract, reference validation을 추적 가능하게 만든다. + +**트레이드오프**: 사용자는 기존 Abaqus input file을 그대로 사용할 수 없을 수 있다. 대신 지원 범위와 실패 원인이 명확해진다. + +### ADR-004: Domain, AnalysisModel, DofManager, AnalysisState를 분리한다 +**결정**: `Domain`은 `ElementDefinition`, `ElementProperty`, `Material`과 +`StepDefinition` 입력 모델 정의를 `std::unique_ptr`로 단독 소유하고 const access와 +stable collection index를 제공한다. `AnalysisModel`은 Domain 수명 안에서 stable index와 +const reference만 사용하는 non-owning current-step view다. `DofManager`는 equation +numbering과 constrained/free mapping을 전담하고, `AnalysisState`는 해석 중 변하는 +물리량을 소유한다. + +**이유**: 모델 정의, step activation, equation system, transient/nonlinear state가 섞이면 parser, assembler, solver, result writer가 강하게 결합된다. 분리된 상태 모델은 선형 정적 해석에서 시작해 비선형, 동적, thermal coupling으로 확장하기 쉽다. + +**트레이드오프**: 초기 class 수가 늘어난다. V0에서는 interface를 얇게 유지하고 실제 선형 정적 frame에 필요한 state만 구현한다. + +### ADR-005: 공식 결과 파일은 HDF5로 하고 reference 결과는 Abaqus CSV로 둔다 +**상태**: HDF5 authoritative output 결정은 유지하며 reference bundle governance 부분은 ADR-019로 대체됨. + +**결정**: FESA solver의 authoritative result output은 `results.h5` HDF5이다. Abaqus reference results는 기능별 reference model contract가 지정한 `reference//` 아래 CSV 파일로 저장하며, verification은 FESA HDF5 rows와 Abaqus reference CSV rows를 documented IDs, components, units, coordinate system, step/frame identity, tolerance 기준으로 비교한다. 신규 reference는 canonical 파일명을 사용하고, 승인된 기존 bundle의 legacy alias는 해당 기능 계약에 정확한 경로를 기록한 경우에만 허용한다. + +**이유**: 구조해석 결과는 step/frame, field/history, node/element/integration point location, units, coordinate system, schema version을 함께 가져야 한다. HDF5는 이 계층 구조와 metadata를 안정적으로 표현한다. + +**트레이드오프**: reference comparison은 FESA HDF5 dataset identity와 Abaqus CSV row identity를 모두 관리해야 한다. FESA HDF5에서 추출한 deterministic CSV view는 디버깅/검토용 보조 artifact로 허용하지만, 공식 solver output이나 reference artifact로 취급하지 않는다. + +### ADR-006: 해석 알고리즘과 수치 backend는 Strategy와 Adapter 경계 뒤에 둔다 +**결정**: `Analysis`, `LinearSolver`, `TimeIntegrator`, `ConvergenceCriteria`는 Strategy로 구성하고, MKL, TBB, HDF5 API는 adapter 계층 뒤에 둔다. `Vector`는 contiguous `double`, `Matrix`는 row-major contiguous `double`을 소유하고 dense 연산은 MKL CBLAS를 사용한다. `SparseMatrix`는 이들과 상속 관계가 없는 0-based CSR 전용 타입이며, PARDISO factorization과 substitution은 `LinearSolver` 경계 뒤에서 분리한다. + +**이유**: 선형 정적, 비선형 정적, 동적, frequency, heat transfer 해석은 공통 흐름을 공유하지만 알고리즘과 backend가 다르다. 외부 API를 core에 노출하면 테스트 double, backend 교체, dependency review가 어려워진다. + +**트레이드오프**: 단일 기능만 구현할 때는 adapter가 다소 장황해 보일 수 있다. Row-major dense storage와 CSR sparse storage를 따로 유지해야 하지만 backend 의존성과 dense/sparse 의미가 core 모델에 섞이지 않는다. + +### ADR-007: Analysis base는 최소 실행 계약만 제공한다 +**결정**: `Analysis` base는 virtual `Analysis::Run(const AnalysisRequest&)`만 제공하고 +linear-static-specific protected hook을 정의하지 않는다. 승인된 선형 정적 순서인 +`parse input -> initialize Domain -> build AnalysisModel -> build DOF map/sparse pattern -> +assemble stiffness -> partition constraints -> factorize Kff -> assemble load -> form effective +RHS -> substitute -> reconstruct displacement -> recover results -> write HDF5`는 +`LinearStaticAnalysis::Run()`의 private lifecycle로 유지한다. 강성행렬 factorization은 +하중벡터 조립보다 먼저 수행하고 factorization과 substitution을 하나의 불투명한 solve +호출로 합치지 않는다. + +**이유**: 현재 여덟 단계는 linear static equation, state와 failure taxonomy에 특화되어 +있다. 최소 base contract는 이 순서의 검증 가능성을 보존하면서 승인되지 않은 dynamic, +eigenvalue 또는 nonlinear procedure에 같은 protected hook과 사용하지 않는 state를 +강제하지 않는다. Factorization과 substitution 분리는 동일 강성행렬에 여러 RHS를 적용할 +수 있고 각 실패 단계를 구조화된 diagnostic으로 분류하게 한다. + +**트레이드오프**: Procedure 사이의 lifecycle code는 base Template Method로 자동 재사용되지 +않는다. 두 번째 procedure가 승인되면 실제로 같은 단계만 focused collaborator로 추출하되, +linear-static hook 사이에 조건문으로 새 physics를 삽입하지 않는다. + +### ADR-008: Sparse assembly는 deterministic COO-to-CSR 경로로 시작한다 +**결정**: 초기 assembly는 element-local contribution을 COO triplet으로 수집한 뒤 CSR로 finalize한다. MKL PARDISO backend는 CSR input contract를 받는다. + +**이유**: CSR은 MKL PARDISO와 잘 맞고, COO-to-CSR 경로는 구현과 테스트가 명확하다. deterministic reduction은 reference comparison의 재현성을 지킨다. + +**트레이드오프**: 대규모 모델에서는 메모리와 변환 비용이 생길 수 있다. 성능 문제가 실제로 확인되면 typed batch assembly 또는 kernel 분리를 추가한다. + +### ADR-009: TBB 병렬화는 element-local computation부터 적용한다 +**결정**: 첫 oneTBB 적용 지점은 element-local matrix/residual 계산이다. 전역 sparse write는 thread-local buffer 또는 deterministic reduction으로 제한한다. + +**이유**: element-local 계산은 독립성이 높고 병렬화 효과가 명확하다. 전역 sparse matrix에 직접 병렬 write하면 재현성, race, ordering 문제가 생긴다. + +**트레이드오프**: 초기 병렬화 범위가 제한된다. MKL 내부 thread와 TBB task arena의 oversubscription 정책을 별도로 문서화해야 한다. + +### ADR-010: Abaqus reference artifact는 사람이 생성하거나 명시 승인된 절차로만 갱신한다 +**상태**: Artifact read-only 및 실행 제한은 유지하며 metadata/provenance/naming 계약은 ADR-019로 대체됨. + +**결정**: Agent는 Abaqus, Nastran 또는 reference solver를 직접 실행하지 않는다. reference artifact 생성, 수정, 복원은 명시 승인된 phase에서만 수행한다. 모든 bundle의 provenance, generator/version, units, coordinate system, step/frame identity, schema, tolerance와 limitations는 승인된 기능별 Reference Model Contract에 기록한다. `metadata.json`은 선택 reference artifact이며, 부재만으로 bundle을 불완전하다고 판정하지 않는다. 파일이 존재하면 read-only 보조 자료로 inventory하고 계약 및 실제 artifact와 일치하는지 확인하며, 충돌은 숨기지 않고 upstream 계약 문제로 보고한다. 승인된 `cantilever-beam-b33` legacy baseline의 space-containing filename과 `README.md` N/A 예외는 유지한다. + +**이유**: reference 결과는 solver correctness의 기준이다. 생성 절차가 불명확하면 구현 결함과 reference artifact 오류를 구분할 수 없다. + +**트레이드오프**: Reference Model Contract가 필수 provenance의 단일 source of truth가 되어 별도 JSON 파일 없이도 bundle을 사용할 수 있다. 선택 `metadata.json`이 계약과 중복될 수 있으므로 존재 시 일관성 검사가 필요하다. Legacy filename과 `README.md` 예외는 일반 artifact 규칙을 복잡하게 하지만 exact path와 source commit을 고정하고 read-only로 취급해 감사 가능성을 유지한다. + +### ADR-011: 구형 단일 검증 진입점 계약을 폐기한다 + +**상태**: ADR-012로 대체됨. + +**결정**: 삭제된 legacy validation entry point, legacy Python test discovery, 환경 변수 기반 command override를 Harness의 기본 검증 계약으로 사용하지 않는다. + +### ADR-012: Harness는 계획, 실행, Hook 검증의 세 계층으로 운영한다 + +**결정**: 계획은 `.agents/skills/harness`, Step 실행과 Git 상태 관리는 `scripts/execute.py`, 도구 호출 전 정책과 종료 전 검증은 `.codex/hooks.json`에 연결된 `scripts/hooks/`가 담당한다. C++ production 변경은 관련 테스트 파일이 있어야 하며, 실제 RED 실패와 GREEN 성공은 같은 Step 안에서 확인한다. Stop은 `.harness/config.json` 또는 자동 감지 결과로 전체 MSVC build/test를 검증한다. Harness Python 변경은 `uv run --with pytest python -m pytest -v -rs`로 검증한다. + +**이유**: 테스트 파일 존재 검사, TDD 실행 증거, 전체 build/test는 서로 다른 책임이다. 이를 분리하면 Hook이 보장하는 범위를 과장하지 않으면서 Step 종료 시 green 상태를 강제할 수 있다. + +**트레이드오프**: PreToolUse만으로 구현 전 RED 실행을 증명할 수 없으므로 Implementation report에 RED/GREEN 명령과 결과를 기록해야 한다. C/C++가 없는 저장소는 Stop이 통과하므로 Harness Python 검증은 별도 acceptance command로 유지한다. + +### ADR-013: V0 선형 정적 해석은 단일 step B33 Euler beam으로 제한한다 + +**결정**: 첫 end-to-end 제품 기능은 입력 파일당 하나의 `*STEP, *STATIC`을 해석하는 2절점 3D Euler–Bernoulli beam이다. Abaqus `TYPE=B33`만 이 요소로 매핑하고 B31은 `unsupported-element-formulation`으로 거부한다. `*PART/*ASSEMBLY/*INSTANCE`는 여러 identity instance와 stable source identity를 지원하지만 instance transform, nested assembly, 다중 step은 지원하지 않는다. Output-request keyword는 승인 allowlist에서 warning 후 no-op 처리하고 FESA 기본 HDF5 결과는 항상 생성한다. + +**이유**: B31은 transverse shear deformation을 포함하므로 Euler–Bernoulli 정식화와 의미가 다르다. 단일 step과 identity instance로 범위를 제한하면 parser wrapper, source identity, assembly, 해법 및 결과 복구를 end-to-end로 검증하면서 의미를 왜곡하지 않는다. + +**트레이드오프**: 기존 Abaqus 모델 중 B31, instance transform, 다중 step 또는 `*DLOAD`를 사용하는 입력은 거부된다. 이후 기능은 별도 요구조건과 formulation/reference gate를 거쳐 추가한다. + +### ADR-014: B33 cantilever baseline은 component-scale 혼합 tolerance로 비교한다 + +**상태**: Artifact inventory와 component mapping의 역사적 결정은 유지하며 tolerance와 +node-station projection 결정은 ADR-022로 대체됨. + +**결정**: `reference/cantilever beam/`의 B33 input, displacement CSV, reaction CSV, elemental-force CSV를 V0 reference baseline으로 사용한다. Displacement와 reaction은 node identity로, section resultant는 CSV node station과 정규화된 FESA element endpoint로 비교한다. 같은 model, step/frame, quantity, component에 대해 `reference_scale = max(abs(reference rows))`를 계산하고 모든 row에 `absolute_floor + 1e-6 * reference_scale`을 적용한다. SI displacement/rotation absolute floor는 `1e-9`, force/moment floor는 `1e-3`이다. Beam stress는 출력하되 Abaqus stress reference comparison은 N/A다. + +**이유**: 자유단 moment처럼 이론적으로 0인 값에는 행별 상대오차가 정의되지 않으며 Abaqus 결과에 작은 수치 잔차가 남을 수 있다. Component scale을 사용하면 전체 물리량 크기에 비해 작은 잔차를 허용하면서 각 row를 결정적으로 판정할 수 있다. + +**트레이드오프**: 큰 component scale은 zero row의 허용폭을 넓힌다. 이를 완화하기 위해 quantity와 component를 섞지 않고 Abaqus 값만 scale에 사용하며, 모든 row의 absolute/normalized error와 worst row를 보고하고 누락·추가·nonfinite row를 tolerance 전에 실패시킨다. + +### ADR-015: 외부 CMake target을 FESA 경계로 정규화하고 Windows runtime closure를 명시한다 + +**결정**: MKL, TBB, HDF5 CONFIG package가 제공하는 target은 dependency module에서 +`Fesa::MKL`, `Fesa::TBB`, `Fesa::HDF5`로 정규화한다. Product target은 이 경계만 +`PRIVATE` link하고 public header에 vendor/Win32 type을 노출하지 않는다. Approved Windows +환경에서는 packaged shared HDF5 C target을 우선하며, CLI와 test executable 옆에 TBB, +MKL, OpenMP, Intel math runtime, HDF5를 포함한 실제 runtime DLL closure를 POST_BUILD로 +stage한다. + +**이유**: Package version이나 shared/static variant에 따라 imported target 이름과 숨은 +runtime dependency가 달라질 수 있다. Configure와 link 성공만 확인하면 GoogleTest +discovery 또는 배포된 CLI가 `0xc0000135`로 실패할 수 있다. FESA-owned target과 명시적 +runtime closure는 module code와 실행 환경을 package 세부사항에서 분리한다. + +**트레이드오프**: Windows staging CMake가 길어지고 새 dynamic dependency마다 runtime +audit가 필요하다. 대신 개인 설치 경로를 source에 하드코딩하거나 developer `PATH`에 +의존하지 않고 build tree executable을 재현 가능하게 실행할 수 있다. + +### ADR-016: 결정론과 failure atomicity를 correctness contract로 취급한다 + +**결정**: Parallel element 계산은 index별 독립 buffer만 쓰고, COO tuple ordering과 +floating-point sum은 join 이후 고정된 serial 순서로 수행한다. Result recovery, solver +output, HDF5 writer는 candidate를 완성·검증한 뒤 caller-owned state 또는 final file에 +commit한다. HDF5는 same-directory temporary file을 checked close/reopen한 후에만 atomic +finalization한다. + +**이유**: Reference comparison과 물리 검토는 row identity와 작은 수치 차이에 민감하다. +Thread scheduling 또는 중간 실패가 sparse bytes, result order, 이전 output을 바꾸면 같은 +입력에 대한 evidence를 신뢰할 수 없다. + +**트레이드오프**: Serial reduction과 candidate copy가 throughput과 memory를 더 사용할 수 +있다. 성능 최적화는 동일한 ordering, failure preservation, reference 결과를 증명할 때만 +대체할 수 있으며, nondeterministic global sparse write는 허용하지 않는다. + +### ADR-017: Essential constraint는 stable elimination으로 적용하고 reaction은 full residual로 정의한다 + +**결정**: V0는 DofManager의 stable free/constrained order로 full stiffness를 +`Kff/Kfc/Kcf/Kcc`로 분할하고 `Kff * df = Ff - Kfc * dc`를 푼 뒤 full displacement를 +복구한다. 모든 DOF가 constrained인 모델의 `0 x 0 Kff`는 유효한 trivial factorization과 +empty substitution이다. Recovery의 full residual `r = K*d - F`를 reaction vector로 +보존하며 constrained entry는 physical reaction, free entry는 equilibrium evidence다. + +**이유**: Equation numbering과 prescribed displacement를 한 owner가 관리하면 nonzero +constraint, no/all/mixed constraint가 같은 수식과 stable order를 사용한다. Full residual은 +element end action을 별도로 합산하는 경로보다 조립·하중·해법 전체의 equilibrium을 직접 +검증한다. + +**트레이드오프**: Full K와 네 partition을 유지하는 메모리 비용이 있고 reaction vector의 +free entry가 정확한 0이라고 가정할 수 없다. V0에는 penalty, MPC, Lagrange multiplier를 +혼합하지 않으며, 추가 constraint policy는 별도 formulation과 equation/output 계약을 +요구한다. + +### ADR-018: Kernel 존재와 제품 노출을 분리하고 계층형 검증을 요구한다 + +**결정**: Local element/load/recovery kernel이 존재해도 syntax, semantic Domain mapping, +analysis orchestration, mandatory HDF5와 diagnostic 계약이 연결되지 않으면 CLI 지원으로 +간주하지 않는다. 검증은 unit, integration, reference comparison, physics sanity를 서로 +대체할 수 없는 계층으로 유지한다. Reference가 가능하지 않은 quantity는 명시적 N/A와 +analytical/schema/physics 대체 evidence를 요구한다. + +**이유**: Unit test는 수식과 validation을, integration test는 lifetime과 호출 순서를, +reference comparison은 외부 solver와의 수치 일치를, physics sanity는 공통-mode 오류와 +model adequacy를 검출한다. 한 계층의 성공만으로 parser exposure 또는 물리 correctness를 +추론하면 지원 범위와 검증 강도가 과장된다. + +**트레이드오프**: 작은 기능도 여러 계약과 evidence를 함께 준비해야 하므로 개발 속도가 +느려진다. 대신 `*DLOAD`처럼 kernel은 있지만 입력에 노출되지 않은 기능, stress처럼 +mandatory output이지만 Abaqus reference가 N/A인 기능을 정확하게 표현할 수 있다. + +### ADR-019: Abaqus는 입력 형식과 외부 수치 reference이며 FESA 내부 동작 계약이 아니다 + +**결정**: FESA는 Abaqus와 독립적인 솔버다. 기능별 승인 `.inp` subset을 입력으로 +사용하고, 기능이 blocking으로 선언한 FESA HDF5 quantity만 기존 Abaqus CSV와 승인 +tolerance로 비교한다. Abaqus 요소 정식화, 적분, stabilization, 내부 상태와 결과 생성 +절차를 재현하거나 동등하게 구현하지 않는다. Exact numerical equality는 허용되지만 +내부 동작 동등성의 evidence가 아니다. + +Reference case readiness에는 선언된 `.inp`, 실제 비교에 필요한 CSV, deterministic +source-ID/component matching과 tolerance만 필요하다. 기존 path와 filename을 그대로 +사용하며 canonical naming, legacy-alias 승인, bundle `README.md`, `metadata.json`, Abaqus +version/provenance, 중복 units/coordinates/model/step/frame/material/section 정보와 CSV schema +version은 요구하지 않는다. Reference artifact는 계속 read-only이며 누락, 추가, 중복, +nonfinite required row는 tolerance 전에 실패한다. + +**이유**: Reference comparison의 목적은 FESA의 독립 정식화가 승인된 observable quantity를 +충분히 가깝게 계산하는지 판정하는 것이다. 수치 비교에 사용되지 않는 artifact +거버넌스가 formulation review나 implementation planning을 차단하면 제품 검증보다 문서 +형식 준수가 우선된다. 같은 정보는 `.inp`, CSV header와 feature contract에서 직접 얻을 +수 있다. + +**트레이드오프**: Reference 생성 환경을 사후에 완전히 재구성하는 감사 기능은 줄어든다. +대신 비교 대상과 source-row/component mapping, tolerance, artifact immutability는 유지해 +false match와 결과 보정을 방지한다. 더 강한 provenance가 필요한 기능은 해당 요구조건이 +명시적으로 추가할 수 있으나 프로젝트 기본 gate로 자동 승격하지 않는다. + +### ADR-020: MITC4 displacement reference는 고정 절대오차로 판정한다 + +**상태**: S4 artifact 선택과 U blocking/UR warning-only 분류는 유지하며 고정 절대오차 +결정은 ADR-022로 대체됨. + +**결정**: Full-integration FESA-MITC4의 sole S4 reference comparison은 matched global +`U1/U2/U3` row에 고정 절대오차 `1.0e-5`를 적용해 pass/fail을 판정한다. +`UR1/UR2/UR3`도 고정 절대오차 `1.0e-5`로 비교하지만 초과는 deterministic warning만 +생성한다. Reference scale은 보고용 통계로 남을 수 있으나 MITC4 row tolerance 또는 +normalized error의 분모를 결정하지 않는다. ADR-014의 B33 component-scale 혼합 +tolerance는 변경하지 않는다. + +**이유**: Abaqus S4는 FESA-MITC4와 동일한 요소 정식화가 아니므로 B33에서 승인한 매우 +엄격한 component-scale tolerance를 그대로 재사용하는 것은 독립 솔버의 observable +displacement 검증 목적에 맞지 않는다. 고정 절대오차는 현재 사용자 단위계에서 승인된 +차이를 명시적으로 제한하면서 zero clamp나 row omission 없이 모든 displacement row를 +동일하게 판정한다. + +**트레이드오프**: Model scale이 크게 달라지면 고정 절대오차의 상대적 엄격도가 달라질 수 +있다. 따라서 이 값은 현재 승인된 MITC4 S4 case의 기능 완료 기준이며 개발 완료 후 +별도 reference-verification evidence와 함께 재점검한다. + +### ADR-021: Semantic definition과 runtime solver contract를 분리한다 + +**결정**: Domain-owned semantic definition과 analysis-time numerical object를 다음 +dependency 방향으로 분리한다. + +```text +Domain owns ElementDefinition / ElementProperty / Material / StepDefinition +AnalysisModel is a non-owning stable-index view into Domain +ElementFactory creates runtime Element candidates from compatible definitions +DofManager -> ElementDofLayout +SparseAssembler -> ElementStiffnessContribution +ResultRecovery -> ElementResultBundle +LoadAssembler -> ordered LoadContribution +EssentialConstraintPolicy -> ConstraintDefinition +Analysis <- LinearStaticAnalysis +``` + +`Domain`의 polymorphic semantic collection은 `std::unique_ptr` 단독 ownership과 stable +vector position을 사용한다. `ElementDefinition`은 source identity, connectivity와 +property/material identity를 제공하고 runtime `Element`는 DOF layout, stiffness와 result +recovery를 제공한다. `ElementFactory`가 definition/property/material compatibility를 +중앙에서 검사하며 unknown 또는 incompatible 조합은 fail-closed diagnostic으로 거부한다. +Consumer는 B33/MITC4 concrete type branch를 분산시키지 않고 runtime contract를 사용한다. +Linear-static candidate가 runtime `Element`를 `std::unique_ptr`로 소유하고 consumer는 그 +수명에 한정된 non-owning view만 사용한다. + +`Material` base에는 identity, source location과 lifetime 이외의 future capability를 +추가하지 않는다. 현재 isotropic linear elasticity가 실제로 요구하는 data는 concrete +material에 둔다. Density, anisotropy, plastic state, temperature와 rate dependency는 +optional field 또는 no-op virtual method로 미리 할당하지 않는다. `ElementProperty`도 +현재 beam/shell 의미를 각 concrete type에 둔다. + +`Load`는 ordered `LoadContribution`을 생성하고 global full-DOF accumulation은 +`LoadAssembler`가 단독 소유한다. `BoundaryCondition`은 `ConstraintDefinition`을 생성하고 +`EssentialConstraintPolicy`가 prescribed-displacement elimination과 reconstruction을 +소유한다. Distributed/body load와 MPC/penalty/Lagrange-multiplier enforcement는 이번 +결정으로 구현된 기능이 아니다. + +Abaqus Domain mapper, result recovery와 HDF5 writer는 기존 public facade를 유지하면서 +각각 topology/material-property/step-final-assembly, equilibrium/beam/shell/atomic-state, +RAII/model-result-dataset/self-check/atomic-finalization 책임으로 private implementation을 +나눈다. + +**이유**: Semantic identity와 runtime kernel을 같은 concrete record에 두면 DofManager, +SparseAssembler, ResultRecovery, parser와 output이 B33/MITC4 storage를 함께 알아야 한다. +Definition/factory/runtime contract와 contribution/policy 경계를 분리하면 stable source +identity와 deterministic reduction owner를 유지하면서 실제 두 element 구현을 공통 +consumer로 연결할 수 있다. Focused facade 분할은 외부 계약을 바꾸지 않고 큰 translation +unit의 서로 다른 failure-atomicity 책임을 검토 가능하게 한다. + +**트레이드오프**: Base object, factory와 contribution record가 늘고 checked compatibility에 +한 단계의 indirection이 생긴다. 대신 `std::shared_ptr`, speculative `Clone()`, global +registry와 future-only material/analysis capability는 도입하지 않는다. B33/MITC4의 승인된 +formulation, 연산·reduction 순서, sign, units, coordinates와 result identity가 이 +리팩터링보다 우선하며 HDF5 schema, reference artifact와 ADR-014/ADR-020 tolerance는 +변경하지 않는다. MITC3, solid, dynamic과 plastic behavior는 별도 feature gate 전까지 +구현된 것으로 간주하지 않는다. + +### ADR-022: 외부 reference comparison은 공통 family-scale 정책을 사용한다 + +운영 상수, 검증 순서, report schema와 변경 관리는 `docs/TOLERANCE.md`를 따른다. + +**결정**: B33, MITC4와 이후 기능의 외부 reference comparison은 동일한 무차원 수치 +정책을 사용한다. 같은 model/case, step/frame, logical quantity, unit dimension, coordinate +system과 blocking behavior의 component family에 대해 reference-only scale +`S=max(abs(reference))`를 계산한다. `abs(reference)<=0.01*S`인 행은 +`abs(fesa-reference)<=0.01*S`, 나머지 행은 `abs(fesa-reference)/abs(reference)<=0.05`를 +만족해야 한다. 모든 행과 함께 family scale-relative RMS `RMS(error)/S<=0.01`도 +통과해야 한다. 독립 absolute-error gate는 두지 않으며 `S=0`이면 모든 FESA 값도 exact +zero일 때만 통과한다. + +B33 family는 translation, rotation, reaction force, reaction moment, section force와 +section moment로 구분한다. 2026-08-18에 다시 생성된 elemental-force CSV의 +`Element Label`과 `Node Label`을 사용해 `(instance, element, endpoint node, component)`를 +HDF5 endpoint에 직접 대응하고 node-station collapse나 평균을 사용하지 않는다. MITC4는 +translation `U1/U2/U3` family를 blocking으로, rotation `UR1/UR2/UR3` family를 +warning-only로 유지한다. + +**이유**: Feature별 absolute floor와 fixed absolute tolerance는 모델 단위와 크기에 따라 +서로 다른 엄격도를 만들었다. 개별 component scale은 물리적으로 zero-like인 MITC4 +`U1/U2` residue를 자체 scale로 만들어 잘못 실패시킨다. 동일 차원의 family scale, +near-zero 대체 분기와 relative RMS를 결합하면 zero row의 불안정한 raw relative error를 +피하면서 모든 행과 전체 오차 수준을 함께 검사할 수 있다. B33의 element-endpoint identity는 +reference와 HDF5가 제공하는 실제 source identity를 보존한다. + +**트레이드오프**: Family 최대값이 작은 component의 허용폭을 결정하므로 행별 relative +gate와 family RMS gate를 모두 유지해야 한다. Reference family 전체가 zero이면 별도 +절대 scale이 없으므로 exact-zero만 허용하는 엄격한 fail-closed 정책이 된다. 기존 B33과 +MITC4 verification report 및 comparator schema는 새 정책에 맞춰 다시 생성해야 하지만 +reference artifact, HDF5 schema, element formulation과 blocking/warning 분류는 변경하지 +않는다. + +### ADR-023: Nonlinear Static은 prepared dispatch와 accepted-frame evidence로 분리한다 + +**결정**: CLI는 하나의 `.inp` operand만 받고 input lexical sibling +`_results.h5`와 `_log.txt`를 유도한다. `--output`은 지원하지 않는다. +`PrepareAnalysisInput()`이 syntax/semantic map을 한 번만 수행하고, prepared Domain의 +procedure identity가 linear static 또는 separate nonlinear full-Newton procedure를 선택한다. +Nonlinear procedure는 trial/committed state, unsymmetric tangent solver와 iteration log를 +독립적으로 소유하며 log close 뒤에만 schema-v1 HDF5 writer가 accepted frames를 atomic publish한다. + +schema-v1 `/global/equilibrium`의 six values는 global origin `[0,0,0]`에서 current `X+u` +force arm과 spatial physical nodal moments를 쓴 applied-plus-constrained-reaction balance +error다. Accepted free generalized residual과 constrained-reaction resultant는 distinct +projections이며 result dataset을 서로 대체하지 않는다. Nonlinear reference portfolio는 fine +primary `shellNL-meshfine`, coarse companion `shellNL`, linear control `shell`을 분리한다. +External U is blocking, UR is warning-only, local RF/RM is review-only, and RM3/stress is excluded; +the factor-one internal global force/moment balance and accepted free residual remain blocking. + +**이유**: Input parse, linear lifecycle, nonlinear trial state와 writer finalization을 한 +procedure에 섞으면 output lifetime과 failure atomicity를 검증할 수 없다. Applied-plus-reaction +balance, constrained reaction과 free residual을 구분하면 nonzero prescribed translation/rotation +case에서도 force arm, mask와 rotation-coordinate conversion의 의미가 보존된다. + +**트레이드오프**: Nonlinear path는 linear procedure보다 state/backend/writer 수가 늘고 fine +reference run 시간이 길다. 대신 HDF5 authoritative output, immutable CSV artifact와 external +solver internal-algorithm non-equivalence 원칙을 유지하며 local support-force distribution을 +blocking tolerance로 위장하지 않는다. diff --git a/docs/archive/pre-generalization/ARCHITECTURE.md b/docs/archive/pre-generalization/ARCHITECTURE.md new file mode 100644 index 0000000..1a9e02a --- /dev/null +++ b/docs/archive/pre-generalization/ARCHITECTURE.md @@ -0,0 +1,506 @@ +# 아키텍처 + +## 목표 +FESA의 아키텍처 목표는 Abaqus `.inp` subset을 내부 semantic model로 변환하고, 유한요소 equation system을 구성해 구조해석 결과를 HDF5로 저장하며, reference comparison과 physics sanity가 가능한 C++17/MSVC 솔버 구조를 제공하는 것이다. + +FESA의 element formulation과 numerical path는 Abaqus와 독립적이다. Abaqus `.inp`는 +승인된 입력 형식이고 Abaqus CSV는 기능별 blocking quantity의 외부 수치 reference다. +Abaqus 내부 적분, stabilization, state 또는 recovery 동작은 FESA architecture contract가 +아니다. + +핵심 품질 속성: +- FEM formulation traceability +- explicit I/O contracts +- sparse linear algebra backend isolation +- deterministic verification +- incremental feature addition +- Harness 기반 TDD + +현재 승인된 V0 end-to-end 기능은 `linear-static-3d-euler-beam`이다. 상세 계약의 +source of truth는 +`docs/superpowers/specs/2026-08-08-linear-static-3d-euler-beam-design.md`이며, 이 문서는 +그 계약을 전체 solver architecture의 모듈과 데이터 흐름에 배치한다. 아래에 나타난 +비선형, 동적, thermal 및 다른 element 계층은 장기 확장 경계이며 V0 지원 범위가 아니다. + +## 현재 구현된 디렉토리 구조 +```text +include/fesa/ # backend-neutral public C++ API +src/ + fesa/ + analysis/ # Analysis lifecycle, V0 LinearStaticAnalysis + app/ # CLI application and main + assembly/ # deterministic stiffness/load assembly, ParallelFor adapter + constraints/ # essential-constraint elimination and reconstruction + core/ # source identity, status, diagnostics + elements/ # current B33/MITC4 kernels and recovery + fem/ # DOF/equation numbering and sparse pattern + io/ + abaqus/ # .inp syntax reader and semantic Domain mapper + hdf5/ # private HDF5 writer and atomic finalization + math/ # owning Vector, row-major Matrix, 0-based CSR SparseMatrix + model/ # concrete V0 semantic records and immutable Domain + results/ # recovery records, full residual, ResultsWriter boundary + solvers/ + linear/ # LinearSolver interface and MKL PARDISO adapter +tests/ + unit/ # local contracts and failure paths + integration/ # analysis orchestration and CLI contracts + reference/ # HDF5-to-Abaqus projection and comparison +reference/ + / # declared read-only Abaqus input/required CSV pair +.agents/ + skills/ # Harness and review skills +.codex/ + hooks.json # PreToolUse/Stop hook registration + agents/ # FESA workflow custom agents + skills/ # FESA solver workflow skills +docs/ # Product, architecture, ADR, workflow artifacts +scripts/ + execute.py # Phase step executor + hooks/ # PreToolUse/Stop hook implementations + msvc_harness/ # MSVC project discovery and validation adapters +phases/ # Optional generated phase plans +``` + +`materials/`, `properties/`, `loads/`와 checked `ElementFactory`는 승인된 C++ modular +refactoring의 target boundary이며 후속 implementation Step에서 추가한다. 이 +documentation-only Step 시점에는 현재 구현 디렉토리로 표시하지 않는다. Density, +plasticity, anisotropy, nonlinear/dynamic analysis, MPC/penalty, history output과 production +validation module은 계속 장기 확장 경계이며 별도 승인 기능이 필요하다. + +## Harness Execution Layer + +Harness는 solver core와 분리된 세 계층의 개발 운영 인프라다. + +- 계획 계층: `.agents/skills/harness`가 사용자 승인 전 Step 초안을 만들고, 승인 후 `phases/` 파일을 생성한다. +- 실행 계층: `scripts/execute.py`가 `feat-` 브랜치에서 Step마다 독립 Codex 세션을 실행하고 상태와 커밋을 관리한다. +- 검증 계층: `.codex/hooks.json`이 `scripts/hooks/pre_tool_use.py`와 `scripts/hooks/stop_validation.py`를 연결한다. Stop 검증은 `scripts/msvc_harness/`를 통해 MSVC build와 test를 실행한다. + +Runner는 `git add -A`를 사용하므로 clean worktree 또는 별도 Git worktree가 실행 전제다. 전체 동작 계약은 `docs/HARNESS_WORKFLOW.md`, 설치와 `.harness/config.json` 설정은 `docs/HARNESS.md`를 source of truth로 삼는다. + +## CMake target과 dependency graph + +Root CMake project는 C++17, extension off, CMake 3.25 이상을 요구하고 +`cmake/FesaDependencies.cmake`, `src/fesa`, `tests`를 차례로 구성한다. + +```text +MKL CONFIG package ─> Fesa::MKL ─┐ +TBB CONFIG package ─> Fesa::TBB ─┼─> fesa_solver (STATIC) ─> fesa_cli +HDF5 CONFIG package ─> Fesa::HDF5 ─┘ │ + ├─> fesa_unit_tests +approved local GoogleTest source ─> GTest targets ├─> fesa_integration_tests + └─> fesa_reference_tests +``` + +- `FESA_GTEST_SOURCE_DIR`는 네트워크 fetch 대신 승인된 local GoogleTest source checkout을 가리키는 필수 cache path다. +- MKL, TBB, HDF5는 CONFIG package로 탐지한다. Package search가 설치를 찾지 못하면 `MKL_DIR`, `TBB_DIR`, `HDF5_DIR`를 configure 때 지정한다. +- Package별 imported target 이름은 `Fesa::MKL`, `Fesa::TBB`, `Fesa::HDF5`로 정규화한다. Product module은 vendor target 이름을 직접 선택하지 않는다. +- HDF5 package가 shared와 static C target을 모두 제공하면 approved Windows environment에서는 shared target을 우선한다. Static archive의 숨은 compiler-runtime 요구가 link interface 밖으로 새는 것을 피하기 위한 결정이다. +- `fesa_solver`는 외부 dependency를 `PRIVATE`으로 link하고 `/W4 /WX`를 사용한다. 따라서 public header는 MKL/TBB/HDF5/Win32 type을 포함하지 않아야 한다. +- Test executable은 unit, integration, reference 경계를 분리하며 `fesa_tests` target은 세 executable을 build하는 aggregate target이다. Reference target에만 source/build root compile definition을 제공한다. + +### Windows runtime closure + +Configure 성공은 executable이 GoogleTest discovery 또는 CLI 실행 시 필요한 DLL을 찾는다는 +뜻이 아니다. `fesa_cli`와 세 test executable의 POST_BUILD 단계는 TBB, MKL thread/core/default +dispatch, OpenMP, `libmmd.dll`, HDF5를 포함한 imported-target runtime DLL을 executable 옆에 +복사한다. 새 dynamic backend를 도입할 때는 link 성공뿐 아니라 clean environment에서의 +post-build discovery/실행까지 runtime closure로 다뤄야 한다. 개인 설치 absolute path를 +CMake source에 기록하지 말고 config package와 imported target metadata를 확장한다. + +## 모듈 경계 +- `core`는 외부 라이브러리에 의존하지 않는다. +- `io/abaqus`는 syntax와 semantic mapping만 담당하고 해석 알고리즘을 알지 않는다. +- `model`은 Abaqus keyword 문자열이 아니라 solver semantic model을 가지며 `Domain`과 + non-owning `AnalysisModel`의 수명 경계를 소유한다. +- `materials`와 `properties`는 Domain이 소유하는 semantic identity와 현재 승인된 + isotropic elasticity 및 beam/shell property data만 제공한다. +- `elements`는 Domain-owned `ElementDefinition`, runtime numerical `Element`, checked + `ElementFactory`와 element-local stiffness/recovery contract를 제공한다. +- `fem`의 `DofManager`는 DOF, equation ordering, scatter와 sparse pattern을 소유한다. +- `assembly`는 runtime element contribution과 ordered load contribution을 stable full-DOF + space에 조립한다. Contribution producer는 global storage를 직접 갱신하지 않는다. +- `loads`는 semantic target과 magnitude를 소유하고 ordered `LoadContribution`을 생성한다. +- `constraints`는 `ConstraintDefinition` 생성과 V0 essential BC elimination 및 full/reduced + vector 변환을 분리한다. MPC와 penalty는 현재 범위가 아니다. +- `solvers`는 `LinearSolver` 뒤에 MKL PARDISO 세부 구현을 감춘다. TBB는 `assembly/ParallelFor`, HDF5는 `results/ResultsWriter` 경계 뒤에 각각 격리된다. +- `analysis`는 step/history data를 받아 procedure를 실행하고 solver backend와 result writer를 조율한다. +- `results`는 full residual과 B33/MITC4 rows를 복구하고 backend-neutral writer contract를 + 제공한다. HDF5 schema 구현은 `io/hdf5`가 담당한다. +- test helper는 production parser/solver 내부 상태를 우회하지 않는다. + +## 승인된 리팩터링 dependency와 ownership + +다음 graph는 C++ object-oriented modular refactoring의 구현 방향을 고정한다. `owns`는 +단독 수명 소유권을, 나머지 화살표는 왼쪽 consumer가 오른쪽 contract를 사용한다는 +뜻이다. 이 graph는 기존 B33/MITC4 물리 기능을 늘리지 않는다. + +```text +Domain owns ElementDefinition / ElementProperty / Material / StepDefinition +AnalysisModel is a non-owning stable-index view into Domain +ElementFactory creates runtime Element candidates from compatible definitions +DofManager -> ElementDofLayout +SparseAssembler -> ElementStiffnessContribution +ResultRecovery -> ElementResultBundle +LoadAssembler -> ordered LoadContribution +EssentialConstraintPolicy -> ConstraintDefinition +Analysis <- LinearStaticAnalysis +``` + +승인된 B33/MITC4 formulation, operation/reduction order, sign, units, coordinates와 row +identity는 이 ownership 리팩터링보다 우선한다. HDF5 schema, reference artifact와 +ADR-014/ADR-020 tolerance도 변경하지 않는다. + +`Domain`은 `ElementDefinition`, `ElementProperty`, `Material`과 `StepDefinition` base +object를 `std::unique_ptr`로 단독 소유하고 const access를 제공한다. Collection position은 +기존 stable `EntityIndex` 의미를 유지한다. `AnalysisModel`과 solver consumer는 ownership을 +가져오지 않고 Domain 수명 안에서 stable index 또는 const reference만 사용한다. +`std::shared_ptr`, speculative `Clone()`과 global registry는 이 계약에 포함되지 않는다. + +`ElementDefinition`은 source identity, source element type, connectivity와 +property/material identity를 보존하는 semantic object다. Runtime `Element`는 active +`ElementDofLayout`, `ElementStiffnessContribution`과 `ElementResultBundle`을 제공하는 +numerical kernel이다. `ElementFactory`만 compatible definition/property/material 조합을 +검사해 runtime candidate를 만들며 unknown 또는 incompatible 조합을 기존 +`Status`/`Result` diagnostic으로 fail-closed 처리한다. `DofManager`, +`SparseAssembler`와 `ResultRecovery`는 B33/MITC4 concrete storage가 아니라 위 runtime +contract를 소비한다. Linear-static candidate는 runtime `Element`를 +`std::vector>`로 소유하고, consumer에는 그 owner 수명 안에서만 +유효한 non-owning `ElementView`를 제공한다. + +`Material` base는 identity, source location과 수명 의미만 공유한다. 현재 concrete +isotropic linear elasticity에 필요한 capability만 사용하며 density, anisotropy, plastic +state, temperature 또는 rate dependency를 optional field나 no-op method로 미리 추가하지 +않는다. `ElementProperty`도 beam/shell이 실제 사용하는 data만 각 concrete type에 둔다. + +`Load`는 semantic target과 magnitude를 소유하고 source order가 보존된 +`LoadContribution`을 생성한다. Global vector의 deterministic accumulation은 +`LoadAssembler`만 수행한다. `BoundaryCondition`은 `ConstraintDefinition`을 생성하고 +`EssentialConstraintPolicy`가 prescribed displacement의 stable elimination과 +reconstruction을 수행한다. Future distributed/body load와 MPC enforcement는 구현된 +기능이 아니며 별도 승인 계약 없이 이 경계에 branch나 optional state를 추가하지 않는다. + +책임이 큰 facade는 외부 계약을 유지한 채 private implementation만 다음 owner로 나눈다. + +- Abaqus Domain mapping: topology, material/property, step/load/boundary mapping과 final + Domain assembly +- Result recovery: global equilibrium, beam recovery, shell recovery와 atomic state commit +- HDF5 writing: RAII/primitives, model datasets, result datasets, self-check와 atomic + finalization + +이 분할은 parser diagnostic, `ResultsWriter` boundary, HDF5 schema 또는 final-file +atomicity를 변경하지 않는다. + +## V0 입력 경계 + +V0 parser는 keyword와 parameter를 case-insensitive하게 해석하되 source label의 원문을 +보존한다. 지원하는 model/procedure keyword는 다음과 같다. + +- `*HEADING` +- `*PART`, `*END PART` +- `*NODE` +- `*ELEMENT, TYPE=B33` +- `*NSET`, `*ELSET`, including `GENERATE` +- `*MATERIAL`, `*ELASTIC` +- `*BEAM GENERAL SECTION, SECTION=GENERAL` +- `*SECTION POINTS` +- `*ASSEMBLY`, `*END ASSEMBLY` +- `*INSTANCE`, `*END INSTANCE` +- `*BOUNDARY`, `*CLOAD` +- `*STEP`, `*STATIC`, `*END STEP` + +Part 내부 label은 `SourceEntityId { instance_name, source_label }`로 보존하고 Domain은 +별도 stable internal index를 부여한다. 같은 part의 여러 identity instance는 허용하지만 +translation/rotation data와 nested assembly는 구조화된 unsupported diagnostic으로 +거부한다. 입력 파일당 하나의 static step만 허용하고 두 번째 step은 오류다. + +`*PREPRINT`, `*RESTART`, `*TRANSVERSE SHEAR STIFFNESS`, `*OUTPUT, FIELD`, +`*OUTPUT, HISTORY`, `*NODE OUTPUT`, `*ELEMENT OUTPUT`, `*CONTACT OUTPUT` 및 그에 속한 +미지원 output variable data는 warning 후 no-op 처리한다. 이 allowlist 밖의 미지원 +model-affecting keyword는 오류다. FESA output은 Abaqus output request에 좌우되지 않는다. + +## V0 beam과 section 계약 + +`EulerBeam3D`는 2절점과 절점당 `[UX, UY, UZ, URX, URY, URZ]` 6 DOF를 사용하며 축, +두 방향 Euler–Bernoulli 굽힘과 Saint-Venant 비틀림을 제공한다. Beam tangent를 local +`x`, Abaqus first section axis `n1`을 local `y`, `t x n1`을 local `z`로 둔다. +`*BEAM GENERAL SECTION`의 `A, I11, I12, I22, J`는 `Iy=I11`, `Iz=I22`로 매핑하고 +V0는 `I12=0`만 허용한다. `E`, `G`, `A`, `Iy`, `Iz`, `J`는 양수여야 하며 zero-length +element와 tangent에 평행한 guide vector를 scale-aware tolerance로 거부한다. + +요소 API는 stiffness, transformation, load와 recovery 책임을 분리한다. + +```cpp +Matrix localStiffness() const; +Matrix globalStiffness() const; +Vector localEquivalentLoad(const ConstantLocalLineLoad& load) const; +BeamRecovery recover(const Vector& globalElementDisplacement) const; +``` + +`localEquivalentLoad`는 formulation의 constant local line-load kernel을 unit test하기 +위한 계약이다. V0 parser는 `*DLOAD`나 distributed-load Domain object를 생성하지 않으므로 +CLI pipeline에서는 이 kernel을 호출하지 않는다. Stiffness와 recovery는 formulation의 +2점 Gauss rule과 부호 규약을 따른다. + +## 핵심 객체 모델 + +```text +Domain +├── owns nodes, sets and source identity +├── owns unique_ptr +├── owns StepDefinition with Load / BoundaryCondition definitions +└── owns source path/identity and mapping warnings + +AnalysisModel +├── non-owning stable-index view into Domain +├── stable active element/BC/load definition indices +└── reachable material/property indices + +ElementFactory +└── creates checked runtime Element candidates + +Element +├── exposes ElementDofLayout +├── produces ElementStiffnessContribution +└── recovers ElementResultBundle + +DofManager +├── owns node x [UX,UY,UZ,URX,URY,URZ] full-DOF numbering +├── owns stable free/constrained order and prescribed values +├── owns element scatter maps +└── owns full-space CSR structural pattern + +AnalysisState +├── displacement U +├── external force Fext +├── internal force Fint +├── residual R and full-index reaction +├── StepFrameIdentity +└── endpoint, Gauss and S11 recovery rows + +Analysis +└── LinearStaticAnalysis + +Vector +Matrix +SparseMatrix +``` + +위 abstract boundary는 현재 B33/MITC4, isotropic linear elasticity, beam/shell property, +concentrated nodal load, prescribed displacement와 linear static procedure를 연결하는 데 +필요한 최소 계약이다. MITC3, solid, dynamic, frequency, heat-transfer와 plastic behavior는 +구현된 기능이 아니며 승인된 사용 사례 전에 future-only method나 state를 base에 추가하지 +않는다. + +## 상태 관리 +- `Domain`은 입력 파일에서 만들어진 전체 모델 정의를 `std::unique_ptr`로 단독 소유한다. + 파싱 이후에는 가능한 한 불변으로 취급하고 stable collection index를 바꾸지 않는다. +- `LinearStaticAnalysis`가 `Domain`을 소유하고, 그 뒤에 `AnalysisModel`, `DofManager`, `AnalysisState`, stiffness/RHS를 순서대로 만든다. 재사용 시에는 역순으로 해제하여 이전 Domain을 가리키는 view를 남기지 않는다. +- `AnalysisModel`은 현재 step에서 활성화되는 해석 객체들의 실행 view이다. `Domain`을 복사하지 않으므로 Domain이 반드시 더 오래 살아야 한다. +- `DofManager`는 자유도와 방정식 번호를 전담한다. `Node` 또는 `Element` 내부에 equation id를 분산 저장하지 않는다. +- `AnalysisState`는 V0 frame에 필요한 다섯 full-DOF vector와 recovery rows만 소유한다. Velocity, acceleration, temperature, iteration history, nonlinear element state는 해당 procedure가 승인될 때 별도 계약으로 추가한다. +- Result recovery는 모든 candidate vector/row를 검증한 뒤 state에 반영한다. 실패한 recovery가 앞선 유효 state를 부분적으로 덮어쓰지 않아야 한다. + +## 데이터 흐름 +```text +Abaqus input file +-> syntax parse and semantic mapping +-> immutable Domain 생성 +-> 단일 step AnalysisModel view 생성 +-> ElementFactory가 compatible definition에서 runtime Element candidate 생성 +-> DofManager가 ElementDofLayout으로 DOF/scatter map/sparse pattern 생성 +-> ElementStiffnessContribution의 deterministic COO-to-CSR 조립 +-> ConstraintDefinition의 stable essential-constraint partition 생성 +-> LinearSolver::factorize(Kff) +-> ordered LoadContribution의 full nodal load vector 조립 +-> effective RHS = Ff - Kfc * dc +-> LinearSolver::solve(rhs, df) substitution +-> full displacement 복구 +-> full residual/reaction = K*d - F 및 ElementResultBundle 복구 +-> ResultsWriter로 derived `_results.h5` atomic finalization +``` + +### 승인된 Nonlinear Static 경로 + +`PrepareAnalysisInput()`은 input syntax와 semantic Domain을 정확히 한 번 만든다. +`ExecutePreparedAnalysis()`는 sole static step의 procedure identity로 linear static 또는 +nonlinear static executor를 선택하며 두 procedure의 mutable state를 공유하지 않는다. +Nonlinear executor는 committed/trial `NonlinearAnalysisState`, full unsymmetric CSR tangent +solver와 closed-before-finalization Newton iteration log를 소유한다. Completion 뒤 +`FinalizePreparedAnalysisResult()`가 selected backend-neutral writer로 only accepted frames를 +atomic finalization한다. + +Nonlinear schema v1의 factor-one `/global/equilibrium`은 raw full-residual sum이 아니라 +global origin `[0,0,0]`에서 current `X+u` force arm과 independently spatialized applied and +masked constrained-reaction moments를 합한 applied-plus-reaction balance error다. Accepted +free residual은 generalized residual의 free mask projection이고 constrained-reaction resultant는 +별도 review summary다. 어느 quantity도 다른 quantity의 substitute가 아니다. + +강성행렬 factorization은 하중벡터 조립보다 먼저 수행한다. 반력은 element end action의 +별도 합이 아니라 조립된 전체 residual에서 구한다. Constrained component는 physical +reaction이고 free component는 equilibrium residual evidence로 full-index vector에 남긴다. + +## 해석 실행 흐름 + +`Analysis` base는 procedure-specific protected hook을 정의하지 않고 최소 실행 계약인 +`Analysis::Run(const AnalysisRequest&)`만 제공한다. 다음 여덟 단계의 순서와 fail-fast +경계는 `LinearStaticAnalysis::Run()`의 private lifecycle이며 다른 procedure에 강제되지 +않는다. + +| 순서 | Private stage | 주요 작업과 생성되는 소유 객체 | 순서/실패 불변식 | +| --- | --- | --- | --- | +| 1 | `Initialize(request)` | `.inp` syntax read, semantic map, owned immutable `Domain`, sorted warnings | 이전 run의 dependent object를 역순으로 제거하고 parse/map 실패를 input category로 반환한다. | +| 2 | `BuildAnalysisModel()` | non-owning `AnalysisModel` view | Domain을 복사하지 않으며 Domain lifetime 안에서만 사용한다. | +| 3 | `BuildDofMapAndSparsePattern()` | `DofManager`, zero-initialized `AnalysisState` | Stable full/free/constrained numbering과 structural pattern을 한 소유자에게 둔다. | +| 4 | `AssembleAndPartitionStiffness()` | full CSR K와 `Kff/Kfc/Kcf/Kcc` | Element-local buffer를 deterministic하게 reduce하고 structural zero와 stable order를 보존한다. | +| 5 | `Factorize()` | retained `Kff` factorization | 모든 load assembly보다 먼저 호출한다. Valid fully constrained model의 `0 x 0 Kff`는 trivial success다. | +| 6 | `AssembleLoadsAndEffectiveRhs()` | full F와 `Ff-Kfc*dc` | Semantic load source order와 finite sum을 보존하며 solver를 호출하지 않는다. | +| 7 | `SubstituteAndReconstruct()` | free solution과 full displacement | Factorization을 재수행하지 않고 substitution한 뒤 prescribed value를 stable order로 복구한다. | +| 8 | `RecoverAndWriteResults()` | full residual/reaction, B33/MITC4 rows, final HDF5 | Recovery candidate를 원자적으로 commit하고 writer 성공 뒤에만 최종 output을 교체한다. | + +비선형 정적 및 동적 해석은 V0 범위가 아니며 별도 ADR과 formulation을 승인한 뒤 이 +lifecycle과 state/equation 계약을 별도 procedure에 정의한다. LinearStaticAnalysis의 +private stage 사이에 조용히 반복·증분·시간 적분 동작을 삽입하지 않는다. + +## 설계 패턴 +- Strategy/Adapter Pattern: `Analysis::Run(const AnalysisRequest&)`, `LinearSolver`, + `ParallelFor`, `ResultsWriter`가 현재 승인된 public 실행/backend 경계다. Vendor API는 + concrete adapter implementation 안에만 둔다. +- Procedure-owned lifecycle: `Analysis`는 protected Template Method hook을 공유하지 않고 + `LinearStaticAnalysis`가 승인된 여덟 단계 lifecycle을 private하게 소유한다. +- Syntax/Semantic separation: `AbaqusInputReader`는 syntax record를 만들고 `AbaqusDomainMapper`가 승인된 keyword 의미를 concrete Domain record로 변환한다. +- Runtime Polymorphism: backend와 승인된 analysis/element/material/property/load/boundary + 경계에만 사용한다. Factory compatibility는 중앙에서 fail-closed로 검사하며 global + registry 또는 future-only capability를 추가하지 않는다. +- RAII: MKL handle, HDF5 file/dataset, temporary solver workspace의 수명과 오류 처리를 wrapper에 묶는다. + +## Sparse Matrix Policy +- Assembly는 element마다 index-addressed contribution buffer를 만들고, join 뒤 COO tuple을 stable order로 정렬해 한 thread에서 순서대로 합산한 후 CSR로 finalize한다. +- `SparseMatrix`는 0-based CSR 데이터를 소유하는 별도 타입이며 dense `Matrix`를 + 상속하지 않는다. MKL PARDISO backend는 adapter 경계에서 필요한 descriptor와 indexing을 + 변환한다. +- CSR row offset, sorted-unique column, dimensions, finite values를 construction boundary에서 검증하며 structural zero를 삭제하지 않는다. +- Parallel worker는 global sparse matrix나 shared reduction accumulator를 직접 갱신하지 않는다. Worker별 failure slot과 contribution만 쓰고, deterministic reduction은 join 뒤에 수행한다. +- Matrix symmetry와 factorization/substitution failure는 solver adapter가 구조화된 solver diagnostic으로 번역한다. 승인되지 않은 regularization이나 fallback으로 singularity를 숨기지 않는다. + +## Dense Math Policy + +- `Vector`는 contiguous `double` 데이터와 크기를 소유하고 copy, dot, Euclidean norm, + scale, axpy를 MKL CBLAS adapter로 수행한다. +- `Matrix`는 row-major contiguous `double` 데이터와 dimensions를 소유하고 + matrix-vector 및 matrix-matrix 연산에 `CBLAS_ROW_MAJOR`를 사용한다. +- 두 타입은 copy/move semantics와 bounds-checked access를 제공한다. +- MKL header와 MKL-specific type은 adapter implementation 밖으로 노출하지 않는다. + +## Parallel Policy +- `ParallelFor`는 `[0,count)` index-addressed 독립 작업만 노출하며 `SerialParallelFor`와 `TbbParallelFor`가 같은 observable contract를 가진다. +- 첫 oneTBB 적용 지점은 element-local stiffness 계산이다. 각 callback은 자기 index의 output slot만 쓴다. +- 전역 sparse write와 부동소수 reduction은 parallel callback 밖의 deterministic 단계로 제한한다. +- MKL 내부 thread와 TBB element loop가 oversubscription을 만들지 않도록 thread count와 task arena 정책을 명시한다. + +## HDF5 Result Schema +```text +/metadata +/model/nodes +/model/elements +/steps//frames/0/nodal/displacement +/steps//frames/0/nodal/reaction +/steps//frames/0/element/end_force_local +/steps//frames/0/element/section_resultant +/steps//frames/0/element/generalized_strain +/steps//frames/0/element/stress_s11 +/diagnostics +``` + +Schema requirements: +- schema version, units, coordinate system, solver version, source input identity를 metadata에 기록한다. +- field output과 history output을 구분한다. +- reference comparison을 위한 row identity는 node id, element id, integration point id, step/frame id를 포함한다. +- FESA solver는 input lexical sibling `_results.h5`를 authoritative output으로 쓰며, + 같은 directory의 `_log.txt`는 procedure log다. +- Abaqus reference results는 기능 문서가 선언한 기존 CSV 파일이다. Directory/file + naming, README, metadata 또는 provenance는 architecture readiness 조건이 아니다. +- Verification은 기능이 요구하는 source identity와 component를 결정적으로 대응시키고 + 승인 tolerance를 적용한다. Missing/extra/duplicate/nonfinite required row는 숫자 비교 + 전에 실패한다. 단일 step/final-frame 기능은 별도 CSV step/frame 열을 요구하지 않는다. +- FESA HDF5에서 추출한 deterministic CSV view는 optional debugging/review artifact이며 공식 solver output 또는 reference artifact가 아니다. +- Writer는 final과 같은 directory의 임시 HDF5에 전체 schema를 쓴 뒤 flush, checked close, + read-only reopen/self-check를 수행한다. Existing final은 `ReplaceFileW`, 새 final은 + `MoveFileExW(..., MOVEFILE_WRITE_THROUGH)`로 완료하며 실패 시 temporary artifact를 + 정리하고 불완전한 파일을 정상 `_results.h5`로 노출하지 않는다. + +## V0 결과 복구와 reference normalization + +- Nodal displacement와 reaction은 global `[UX, UY, UZ, URX, URY, URZ]` 순서다. +- Equilibrium end action은 local `[FX,FY,FZ,MX,MY,MZ]`, endpoint section resultant는 + `[N,T,My,Mz]`, generalized strain/resultant는 두 Gauss point에 기록한다. +- General beam section stress는 section point의 axial `S11`만 복구한다. Section point가 + 없으면 centroid `(0,0)`을 `source=fesa-default`로 기록한다. +- 승인된 `reference/cantilever beam/cantilever beam elemental forces.csv`는 각 B33 + element의 두 endpoint에 `Element Label`, `Node Label`과 `SF1/SM1/SM2/SM3`을 제공한다. + `(instance, element label, endpoint node label, component)` identity로 HDF5 + `[element,endpoint,N/T/My/Mz]`에 직접 대응하고 `SF1 -> N`, `SM1 -> My`, `SM2 -> Mz`, + `SM3 -> T`로 비교한다. External reference comparison은 node-station collapse나 평균을 + 사용하지 않는다. +- Reference tolerance는 같은 model/case, step/frame, logical quantity, unit dimension, + coordinate system과 blocking behavior의 component family에서 Abaqus-only scale + `S=max(abs(reference))`를 구한다. `abs(reference)<=0.01*S`인 행은 absolute error + `<=0.01*S`, 그 외 행은 relative error `<=0.05`로 판정하며 family scale-relative RMS + `RMS(error)/S<=0.01`도 통과해야 한다. 독립 absolute-error gate는 사용하지 않고 + zero-scale family는 FESA도 exact zero일 때만 통과한다. 전체 판정 과정과 report/change + management contract는 `docs/TOLERANCE.md`를 따른다. +- Beam stress는 HDF5 schema와 unit/analytical test로 검증하지만 Abaqus reference + comparison은 N/A다. + +## CLI와 diagnostics + +```powershell +fesa.exe +``` + +CLI는 input과 같은 lexical sibling directory에 `_results.h5`와 +`_log.txt`를 유도하고 `--output`을 거부한다. Exit code는 +`0=success`, `2=usage`, `3=input syntax/semantic mapping`, `4=model validation`, +`5=factorization/substitution`, `6=HDF5 output`으로 고정한다. Diagnostic은 `severity`, +`code`, `file`, `line`, `keyword`, `entity_identity`, `message`를 가지며 stderr에 +deterministic한 순서로 출력한다. + +## 기능 확장 플레이북 + +다음 표는 코드 위치만이 아니라 함께 바뀌어야 하는 계약 경계를 나타낸다. 한 열만 +구현하고 다른 열을 생략하면 internal utility 또는 실험 kernel일 수는 있어도 제품 기능은 +아니다. + +| 기능 유형 | 시작 전에 고정할 것 | 주요 구현 경계 | 함께 검증할 것 | 피해야 할 shortcut | +| --- | --- | --- | --- | --- | +| 새 element/material | DOF, interpolation, constitutive law, integration, local axes/sign, feature-approved validity boundary | Domain record와 mapper, element kernel, DofManager scatter/pattern, SparseAssembler, ResultRecovery | feature-required invariants/tests and blocking reference quantities | 서로 다른 물리를 같다고 주장, source ID와 internal index 혼용, 검증 전 범용 hierarchy 추가 | +| 새 load/constraint | Abaqus target grammar, application order, units, follower 여부, prescribed-value 의미 | Syntax/mapper, Domain target, full-space LoadAssembler 또는 constraint partition, diagnostics | set/direct target ambiguity, multi-instance identity, nonfinite sum, `Ff-Kfc*dc`, reaction | Element load kernel 존재를 parser 지원으로 간주, penalty를 elimination에 몰래 혼합 | +| 새 analysis procedure | governing equation, state variables, increment/time lifecycle, tangent/residual, convergence와 output frame | 별도 Analysis implementation, procedure-specific state/equation owner, solver interface extension | orchestration order, failure atomicity, restart/frame identity, numerical benchmark | V0 hook 사이에 조건문으로 반복/시간 적분 삽입, 사용하지 않는 future state 선할당 | +| 새 numerical backend | matrix/index contract, lifecycle, reusable state, failure taxonomy, thread/runtime policy | 기존 `LinearSolver` 또는 `ParallelFor` interface의 concrete adapter, CMake normalized target | empty/dimension/extreme-scale input, repeated call, failed-output preservation, clean runtime discovery | Vendor type을 public header에 노출, silent fallback/regularization, absolute install path 고정 | +| 새 output/reference quantity | 물리 정의, location, sign, units, coordinates, stable row identity, mandatory 여부, tolerance | Result record/recovery, AnalysisState, ResultsWriter/HDF5 schema, comparator projection | schema dtype/shape, ordering, nonfinite rejection, identity inventory, reference N/A 대체 evidence | 서로 다른 result identity 혼합, station mismatch 평균, output request로 mandatory result 제거 | + +모든 확장은 PRD의 제품 완료 정의와 요구조건→정식화→I/O→구현→reference→physics gate를 +따른다. 기존 feature contract에 없는 범위를 편의상 “Abaqus compatible”이라고 넓히지 않는다. + +## MITC4 확장 경계 + +MITC4가 구현될 때 Abaqus `S4`와 `S4R` source type은 같은 FESA formulation을 선택한다. +Source type은 metadata/diagnostic identity로 보존하지만 FESA integration 또는 hourglass +경로를 선택하지 않는다. 6-DOF embedding의 비물리 drilling coordinate에는 physical +rotational stiffness block의 positive minimum diagonal에 `1e-3`을 곱한 고정 numerical +stabilization만 둔다. Drilling calibration, artificial-energy policy와 별도 drilling result +dataset은 이 기능 범위가 아니다. + +Full-integration FESA-MITC4의 reference comparison은 `reference/shell/` S4의 기존 input 및 +displacement CSV만 사용한다. Global `U1/U2/U3` translation family만 blocking이고 +`UR1/UR2/UR3` rotation family는 warning-only evidence다. 두 family 모두 ADR-022의 공통 +family-scale row/RMS 정책을 사용한다. S4R은 +같은 kernel을 선택하는 source mapping과 metadata를 unit/integration tests로 검증하며 +`reference/shellR/` artifact는 acceptance comparison에 포함하지 않는다. + +Approved nonlinear MITC4 verification은 `reference/shellNL-meshfine/` fine primary, +`reference/shellNL/` coarse companion과 `reference/shell/` linear control을 각각 독립 +copied-input directory에서 실행한다. Factor-one global force/moment balance와 accepted free +residual은 blocking internal evidence다. External `U1/U2/U3`는 blocking, `UR1/UR2/UR3`는 +warning-only이며 local nodal `RF1/RF2/RF3/RM1/RM2`는 deterministic review-only evidence다; +`RM3`와 stress는 nonlinear numerical comparison에서 제외한다. diff --git a/docs/archive/pre-generalization/PRD.md b/docs/archive/pre-generalization/PRD.md new file mode 100644 index 0000000..71b3964 --- /dev/null +++ b/docs/archive/pre-generalization/PRD.md @@ -0,0 +1,179 @@ +# PRD: FESA 구조해석 솔버 + +## 목표 +FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 자체 유한요소 정식화로 구조해석을 +수행하고, step/frame 단위 결과를 input lexical sibling `_results.h5` HDF5로 저장하며, 기능이 지정한 Abaqus +reference CSV 수치와 비교 가능한 C++17/MSVC 솔버를 제공한다. Abaqus의 요소 알고리즘, +적분법, stabilization 또는 내부 결과 생성 절차를 재현하는 것은 목표가 아니다. + +이 프로젝트의 성공 기준은 단순 실행 성공이 아니다. 기능 구현은 요구조건, 정식화, +I/O 계약, C++ build/test와 기능별 blocking reference comparison을 통과해야 완료된다. +Physics sanity와 release readiness는 구현 완료 뒤 별도 배포 판단을 제공하며 Abaqus 내부 +동작 동등성을 요구하지 않는다. + +## 사용자 +- Solver developer: C++17/MSVC/CMake/CTest 환경에서 요소, 재료, 해석 절차, solver backend를 구현한다. +- Verification reviewer: reference artifact, tolerance, physics sanity, release readiness를 검토한다. +- Analyst preparing Abaqus-compatible input subsets: FESA가 지원하는 제한된 `.inp` subset에 맞춰 입력 모델을 준비한다. +- Codex agent workflow operator: project-local agent와 skill을 사용해 요구조건부터 release까지의 gate를 운영한다. + +## 핵심 기능 +1. Abaqus `.inp` keyword subset parser와 내부 `Domain` semantic model 생성 +2. `AnalysisModel`, `DofManager`, `AnalysisState` 기반의 step별 equation system 구성 +3. 선형 정적 해석을 시작점으로 하는 `Analysis` procedure 계층 +4. Stable source identity를 보존하는 concrete V0 node, B33 element, material, section, boundary, load record +5. sparse matrix pattern 생성, 전역 행렬/벡터 조립, 제약조건 적용 +6. `LinearSolver`, `ParallelFor`, `ResultsWriter` interface를 통한 MKL PARDISO, oneTBB, HDF5 backend 격리 +7. HDF5 기반 versioned step/frame field-result 저장과 향후 history-output 확장 경계 +8. FESA HDF5 rows와 `reference//` 아래 Abaqus reference CSV rows의 직접 비교 +9. CMake/MSVC/x64/Debug, CTest 기반 개발 검증 + +Element/material 계층의 일반적인 runtime polymorphism, iterative solver, history output은 장기 확장 방향이다. 현재 제품 계약으로 간주하지 않으며, 실제 사용 사례와 성능·수명 요구가 승인되기 전에 추상 base class를 미리 추가하지 않는다. + +## 사용자 관점 제품 흐름 + +1. 사용자는 승인된 Abaqus `.inp` subset으로 모델과 단일 analysis step을 기술한다. +2. FESA는 syntax를 읽은 뒤 semantic mapping과 model validation을 수행한다. 지원하지 않는 모델 의미는 구조화된 diagnostic과 안정된 exit code로 거부한다. +3. 해석 procedure는 active model view, DOF/equation map과 sparse system을 만들고, essential constraint elimination을 적용해 선형계를 factorize하고 substitution한다. +4. FESA는 full displacement를 복구하고 equilibrium residual/reaction과 element 결과를 계산한다. +5. 성공 시 output request와 무관한 mandatory 결과, metadata와 diagnostic을 versioned lexical sibling `_results.h5`에 기록한다. 실패 시 불완전한 최종 결과 파일을 정상 결과처럼 남기지 않는다. +6. 검증 workflow는 기능이 지정한 HDF5 quantity를 기존 Abaqus CSV의 source identity와 + component에 대응시켜 tolerance를 판정하고, 별도로 equilibrium과 물리적 타당성을 + 검토한다. Reference bundle naming이나 provenance는 수치 비교 gate가 아니다. + +Parser keyword, element kernel, solver backend 또는 output dataset 중 하나만 추가된 상태는 end-to-end 제품 기능이 아니다. 같은 기능의 입력 의미부터 외부 결과와 검증까지 연결되어야 한다. + +## V0 범위 +- 선형 정적 해석 파이프라인 구현 +- 승인된 첫 end-to-end 기능: 2절점 3D Euler–Bernoulli beam, Abaqus `TYPE=B33` +- 입력 파일당 하나의 `*STEP, *STATIC` +- 절점당 자유도 `[UX, UY, UZ, URX, URY, URZ]` +- 축, 두 방향 굽힘, Saint-Venant 비틀림 및 선형 등방 탄성 +- 최소 Abaqus keyword subset: + - `*HEADING` + - `*PART`, `*END PART` + - `*NODE`, `*ELEMENT, TYPE=B33` + - `*NSET`, `*ELSET`, including `GENERATE` + - `*MATERIAL` + - `*ELASTIC` + - `*BEAM GENERAL SECTION, SECTION=GENERAL` + - `*SECTION POINTS` + - `*ASSEMBLY`, `*END ASSEMBLY` + - `*INSTANCE`, `*END INSTANCE` + - `*BOUNDARY` + - `*CLOAD` + - `*STEP`, `*STATIC`, `*END STEP` +- `*PREPRINT`, `*RESTART`, `*TRANSVERSE SHEAR STIFFNESS`, `*OUTPUT, FIELD`, + `*OUTPUT, HISTORY`, `*NODE OUTPUT`, `*ELEMENT OUTPUT`, `*CONTACT OUTPUT`과 그에 속한 + 미지원 variable data는 warning 후 no-op 처리 +- 같은 part의 여러 identity instance와 stable source label mapping +- nonzero prescribed displacement를 포함한 free/constrained partition +- formulation의 constant local line-load equivalent nodal vector kernel. V0 parser와 CLI는 + `*DLOAD`를 지원하거나 이 kernel을 호출하지 않음 +- deterministic COO-to-CSR stiffness assembly +- MKL PARDISO 기반 sparse direct solver +- stiffness factorization과 load-vector substitution 분리 +- displacements, reactions, equilibrium end actions, section resultants, generalized results, + axial `S11` output +- HDF5 result schema v0 +- `fesa.exe ` CLI with lexical sibling result/log names +- 승인된 `reference/cantilever beam/` B33 결과와 displacement, reaction, section resultant + comparison; beam stress reference comparison은 N/A + +## 승인된 Nonlinear Static 확장 + +- `*STATIC, NLGEOM=YES` 단일 step은 parse/map을 정확히 한 번 수행한 뒤 linear static과 + 분리된 full-Newton procedure로 dispatch한다. Nonlinear path는 별도 mutable state, + unsymmetric tangent solver, iteration log writer와 schema-v1 HDF5 writer를 사용한다. +- CLI는 정확히 하나의 ``만 받고 `_results.h5`와 `_log.txt`를 같은 + lexical sibling directory에 유도한다. `--output`은 지원하지 않는다. +- Nonlinear HDF5 schema v1은 committed accepted frame만 기록한다. factor-zero는 metadata + baseline이며 accepted frame이 아니고, successful factor-one row는 unique + `load_factor=1.0` accepted frame이다. +- Factor-one `/global/equilibrium`은 global origin에서 current `X+u` arm과 spatial physical + moment를 사용한 applied-plus-constrained-reaction balance error다. Accepted free residual과 + constrained-reaction resultant는 그 dataset의 대체 quantity가 아니다. +- `shellNL-meshfine`는 primary nonlinear case, `shellNL`은 companion, `shell`은 linear + regression control이다. `U1/U2/U3`는 blocking, `UR1/UR2/UR3`는 warning-only이며 local + `RF/RM`은 review-only (`RM3` 및 stress 제외)로 비교한다. + +## 신규 기능의 제품 완료 정의 + +새로운 element, load, constraint, material, analysis procedure 또는 output quantity는 다음 조건을 모두 만족할 때 FESA 제품 기능으로 취급한다. + +1. 지원 범위와 제외 범위, 실패 category, solver output의 units/coordinates/identity, + blocking reference quantity와 tolerance가 요구조건과 I/O 계약에 명시되어 있다. +2. 필요한 FEM 정식화와 recovery/sign convention이 검토되었고, 수치 위험과 검증 문제를 numerical review가 다룬다. +3. Syntax와 semantic mapping이 내부 모델에 안정된 source identity로 연결되며 잘못된 입력을 fail-closed로 거부한다. Kernel만 제공하는 기능은 parser/CLI 지원 여부를 별도로 표시한다. +4. DOF, sparse pattern, assembly, constraint, solver lifecycle과 mutable state ownership이 기존 아키텍처에 연결되거나 승인된 새 procedure 경계를 가진다. +5. Mandatory HDF5 schema와 diagnostic, CLI 실패 의미가 정의되고 failure path가 기존 state 또는 최종 output을 오염시키지 않는다. +6. Unit test가 local 수식과 validation을, integration test가 orchestration과 외부 계약을, + required reference comparison이 기능별 blocking quantity의 외부 수치 acceptance를 + 각각 증명한다. Reference가 N/A이면 그 이유와 대체 evidence를 명시한다. + +## 기능 요구조건 +| ID | 요구조건 | Acceptance Criteria | Verification Method | +| --- | --- | --- | --- | +| FESA-PRD-001 | FESA는 Abaqus `.inp` full compatibility가 아니라 승인된 keyword subset만 지원해야 한다. | 지원/미지원 keyword가 문서화되고, 미지원 keyword는 구조화된 diagnostic을 남긴다. | I/O contract review, parser unit test | +| FESA-PRD-002 | FESA는 입력 모델을 `Domain`으로 변환해야 한다. | nodes, elements, materials, properties, sets, loads, boundary conditions, step definitions가 semantic model에 보존된다. | parser integration test | +| FESA-PRD-003 | FESA는 현재 step의 실행 view를 `AnalysisModel`로 구성해야 한다. | active elements, loads, boundary conditions, properties/materials가 Domain 복사 없이 참조 또는 id view로 연결된다. | analysis model unit test | +| FESA-PRD-004 | FESA는 equation numbering과 constraint/free mapping을 `DofManager`에 집중해야 한다. | Node/Element 내부에 equation id를 분산 저장하지 않는다. | code review, DofManager unit test | +| FESA-PRD-005 | FESA는 해석 중 변하는 물리량을 `AnalysisState`에 저장해야 한다. | V0 displacement, external/internal force, residual, constrained reaction, step/frame identity와 element recovery rows가 출력에 연결되고 velocity, acceleration, temperature, iteration history는 할당하지 않는다. | state unit test, integration test | +| FESA-PRD-006 | FESA는 solver 결과를 derived HDF5 authoritative output `_results.h5`로 저장해야 한다. | V0 step/frame field results, metadata, diagnostics가 schema version과 함께 저장된다. | HDF5 schema test | +| FESA-PRD-007 | FESA는 기능이 지정한 Abaqus reference CSV quantity와 비교 가능한 deterministic source-row/component mapping을 제공해야 한다. | Required source IDs/components는 일대일 대응되고 missing/extra/duplicate/nonfinite row는 tolerance 전에 실패한다. Canonical naming, README, metadata, provenance 또는 CSV schema version은 요구하지 않는다. | reference comparison report | +| FESA-PRD-008 | FESA의 production C++ 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현해야 한다. | 관련 C++ test file, RED 실패와 후속 GREEN 성공 증거가 있고 Stop의 전체 MSVC build/test가 통과한다. | implementation report, Hook guardrail, CTest | +| FESA-PRD-009 | FESA는 외부 라이브러리 API를 solver core에 직접 노출하지 않아야 한다. | MKL, TBB, HDF5 의존은 adapter module에 제한된다. | architecture review, dependency review | +| FESA-PRD-010 | FESA 기능 구현 완료는 build/test와 기능별 blocking reference comparison 통과를 요구해야 한다. | Required CTest가 통과하고 선언된 blocking quantity가 승인 tolerance 안에 있다. Physics sanity와 release readiness는 별도 후속 gate다. | build/test report, reference verification report | +| FESA-PRD-011 | V0는 입력 파일당 하나의 linear static step과 Abaqus B33 3D Euler beam만 해석해야 한다. | 단일 B33 model은 해석되고, B31과 두 번째 step은 구조화된 unsupported diagnostic으로 거부된다. | parser/semantic unit test, CLI integration test | +| FESA-PRD-012 | V0는 Part/Assembly/Instance wrapper와 source identity를 보존해야 한다. | 여러 identity instance의 node/element/set label이 stable internal ID로 deterministic하게 매핑되고 transform 또는 nested assembly는 거부된다. | semantic mapping unit test | +| FESA-PRD-013 | Output-request allowlist는 해석 의미를 변경하지 않아야 한다. | 승인 keyword와 variable data는 warning 후 no-op이고, allowlist 밖 model-affecting keyword는 오류이며 기본 결과는 output request와 무관하게 생성된다. | parser diagnostic test, HDF5 integration test | +| FESA-PRD-014 | 선형 정적 pipeline은 stiffness factorization과 load substitution을 분리해야 한다. | `Kff` factorization이 load vector assembly보다 먼저 수행되고, `rhs=Ff-Kfc*dc` substitution으로 full displacement를 복구한다. | orchestration test, solver-adapter test | +| FESA-PRD-015 | FESA는 V0 결과와 diagnostic을 안정된 외부 계약으로 출력해야 한다. | CLI가 `0=success`, `2=usage`, `3=input`, `4=model`, `5=solver`, `6=HDF5` exit code를 사용하고 diagnostic field가 계약과 일치하며 HDF5가 displacement, reaction, end force, section resultant, generalized strain/resultant, axial `S11`을 포함하고 실패 시 불완전한 최종 파일을 남기지 않는다. | CLI integration test, HDF5 schema/atomicity test | +| FESA-PRD-016 | Dense와 sparse math storage 및 backend 경계를 분리해야 한다. | `Vector`는 contiguous, `Matrix`는 row-major contiguous storage와 MKL CBLAS를 사용하고 `SparseMatrix`는 별도 0-based CSR 타입이며 MKL 타입이 public core API에 노출되지 않는다. | math unit test, dependency review | +| FESA-PRD-017 | 모든 외부 reference comparison은 승인된 공통 family-scale tolerance를 사용해야 한다. | 각 matched row가 near-zero 대체 조건 또는 상대오차 `0.05`를 통과하고 각 family의 scale-relative RMS가 `0.01` 이하이며 missing/extra/duplicate/nonfinite row는 comparison 전에 실패한다. | reference comparison unit/integration test, verification report | +| FESA-PRD-018 | 승인된 B33 reference artifact는 현재 경로의 read-only baseline으로 유지해야 한다. | `reference/cantilever beam/` 파일을 rename, rewrite 또는 보정하지 않고 기능이 선언한 exact path에서 읽는다. | artifact inventory, Git diff review | +| FESA-PRD-019 | B33 beam section과 local axis를 Abaqus 의미에 맞게 매핑해야 한다. | `n1 -> local y`, `t x n1 -> local z`, `Iy=I11`, `Iz=I22`, `I12=0`을 적용하고 nonpositive property, zero-length element, tangent-parallel guide vector를 구조화된 model diagnostic으로 거부한다. | section-mapping unit test, element geometry test | + +## 비기능 요구조건 +- MSVC x64 Debug 환경에서 configure, build, CTest를 검증한다. +- reference test 결과는 deterministic해야 한다. +- HDF5 schema는 versioned contract로 관리한다. +- 모든 외부 reference comparison은 동일 logical quantity와 unit dimension의 component + family별 Abaqus-only maximum scale을 사용한다. Near-zero ratio는 `0.01`, 일반 행 + 상대오차는 `0.05`, family scale-relative RMS는 `0.01`이며 독립 absolute-error gate는 + 사용하지 않는다. +- B33 section resultant는 source element label과 endpoint node label로 직접 대응한다. + MITC4 `U1/U2/U3`만 blocking이고 `UR1/UR2/UR3` 초과는 warning-only다. +- parser, solver, HDF5 writer는 실패 원인을 구조화된 diagnostic으로 보고한다. +- oneMKL, oneTBB, HDF5는 CMake에서 명시 탐지하고 실패 원인을 분류한다. +- 대규모 모델 성능 최적화보다 V0의 명확성, 테스트 가능성, 검증 traceability를 우선한다. + +## Acceptance Gates +1. Requirements approved: 기능 범위, 제외 범위, 입력, 출력, tolerance, 검증 물리량이 정의되어 있다. +2. Research evidence complete: 정식화와 benchmark 근거가 신뢰도와 한계와 함께 정리되어 있다. +3. Formulation reviewed: 약형, shape function, B matrix, constitutive contract, 수치적분, output recovery가 검토되어 있다. +4. I/O contract approved: Abaqus keyword subset, internal model mapping, HDF5 result contract, + required reference quantity의 최소 source-ID/component matching이 승인되어 있다. +5. Tests fail before implementation: C++/integration/reference test를 제품 코드보다 먼저 작성하고 같은 Step 안에서 RED 실패와 후속 GREEN 성공을 확인한다. +6. CMake/CTest pass: MSVC/x64/Debug 기준 configure, build, test가 통과한다. +7. Reference comparison pass: 기능이 blocking으로 선언한 FESA `_results.h5` quantity가 + 선언된 Abaqus CSV와 source identity/component별로 대응되고 승인된 tolerance 안에 있다. +8. Physics sanity pass: equilibrium, reaction consistency, displacement direction, symmetry, section-force consistency와 normalized residual이 검토되어 있다. +9. Release readiness pass: acceptance traceability, known limitations, release notes draft가 준비되어 있다. + +## 제외 사항 +- Abaqus full parser 호환 +- Abaqus B31/Timoshenko beam +- 다중 analysis step과 step 간 load/BC propagation +- instance translation/rotation, nested assembly 및 dependent/independent mesh semantics +- `I12 != 0`, taper, offset, release, curved beam, warping +- Abaqus `*DLOAD` 입력과 Domain distributed-load object +- 승인된 fixed-load-factor nonlinear-static 범위를 벗어난 기하·재료비선형, dynamics, + contact, thermal effects +- transverse shear stress와 torsional shear stress recovery +- beam stress에 대한 Abaqus reference comparison +- Abaqus, Nastran 또는 reference solver 직접 실행 자동화 +- Agent가 Abaqus reference CSV 파일을 임의 생성 또는 수정하는 작업 +- GUI 또는 postprocessor +- Visual Studio `.sln`/`.vcxproj` 전용 MSBuild workflow +- plasticity와 shell end-to-end 구현 diff --git a/scripts/execute.py b/scripts/execute.py index 93070cc..f4a33f7 100644 --- a/scripts/execute.py +++ b/scripts/execute.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다. +Harness Step Executor — phase 내 Step을 순차 실행하고 결과와 파일 소유권을 검증한다. Usage: python scripts/execute.py [--push] @@ -18,7 +18,7 @@ import threading import time import types from datetime import datetime, timezone, timedelta -from pathlib import Path +from pathlib import Path, PureWindowsPath from typing import Optional ROOT = Path(__file__).resolve().parent.parent @@ -32,6 +32,10 @@ class CodexEnvironmentError(HarnessError): """재시도로 해결할 수 없는 Codex CLI 환경 오류.""" +class StepOwnershipError(HarnessError): + """승인된 Step 파일 범위 밖의 변경; 사용자 데이터를 되돌리지 않는다.""" + + def _configure_utf8_stdio(): """Windows 콘솔에서도 Harness 진행 문자를 안정적으로 출력한다.""" for stream in (sys.stdout, sys.stderr): @@ -319,6 +323,9 @@ class StepExecutor: status = step.get("status") if status not in self.STEP_STATUSES: raise HarnessError(f"phase step {expected} has invalid status") + if "owned_paths" in step: + self._step_owned_paths(step) + self._validate_attempt_history(step) if status == "completed": if state != "completed-prefix": @@ -347,6 +354,76 @@ class StepExecutor: if not step_file.is_file(): raise HarnessError(f"phase step file not found: {step_file}") + @staticmethod + def _path_key(path: str) -> str: + return os.path.normcase(path).replace("\\", "/") + + def _step_owned_paths(self, step: dict) -> tuple[str, ...]: + paths = step.get("owned_paths") + if not isinstance(paths, list): + raise HarnessError(f"Step {step['step']} requires an owned_paths file array before execution") + normalized = [] + seen = set() + for path in paths: + if not isinstance(path, str): + raise HarnessError("owned_paths entries must be repository-relative file paths") + path = path.replace("\\", "/") + parts = path.split("/") + if ( + not path or PureWindowsPath(path).drive or path.startswith("/") + or any(part in {"", ".", ".."} or part.endswith((".", " ")) for part in parts) + or any(ord(char) < 32 or char in ':*?[]<>"|' for char in path) + or any(part.casefold() == ".git" or PureWindowsPath(part).is_reserved() for part in parts) + or parts[0].casefold() == "phases" + ): + raise HarnessError(f"owned_paths requires an exact non-control relative file path: {path!r}") + key = self._path_key(path) + if key in seen: + raise HarnessError(f"owned_paths contains duplicate/aliased paths: {path}") + seen.add(key) + normalized.append(path) + return tuple(normalized) + + def _validate_owned_file(self, path: str) -> None: + root = Path(self._root) + candidate = root / path + try: + resolved = candidate.resolve() + resolved.relative_to(root) + if resolved != candidate or candidate.is_symlink() or candidate.is_dir(): + raise ValueError("directory or linked path") + if candidate.exists() and candidate.lstat().st_nlink != 1: + raise ValueError("hard-linked file") + except (OSError, RuntimeError, ValueError) as exc: + raise HarnessError(f"owned_paths must name non-linked files inside the repository: {path}") from exc + + def _validate_attempt_history(self, step: dict) -> None: + if "attempt_history" not in step: + return + history = step["attempt_history"] + if not isinstance(history, list): + raise HarnessError("attempt_history must be an Executor-owned array") + for number, record in enumerate(history, 1): + if ( + not isinstance(record, dict) + or type(record.get("attempt")) is not int or record["attempt"] != number + or "exit_code" not in record + or (record["exit_code"] is not None and type(record["exit_code"]) is not int) + or not isinstance(record.get("reported_status"), str) + or record["reported_status"] not in self.STEP_STATUSES + or not isinstance(record.get("recorded_at"), str) or not record["recorded_at"] + or not isinstance(record.get("reported_reason"), str) + or not isinstance(record.get("reason"), str) + or not isinstance(record.get("diagnostic"), str) + or not isinstance(record.get("classification"), str) + or record["classification"] not in { + "environment", "reported-blocked", "reported-error", "completed", + "inconsistent-result", "transport-timeout", "process-error", "missing-result", + "ownership-violation", "commit-error", + } + ): + raise HarnessError(f"attempt_history contains an invalid record at attempt {number}") + def _snapshot_phase_control_files( self, index: dict ) -> tuple[bytes, dict[str, bytes]]: @@ -539,6 +616,10 @@ class StepExecutor: return match is not None and int(match.group(1)) < self._total def _preflight_repository(self) -> None: + for step in self._read_phase_index()["steps"]: + if step["status"] == "pending": + for path in self._step_owned_paths(step): + self._validate_owned_file(path) self._implementation_agent_profile() top_level_result = self._run_git_checked("rev-parse", "--show-toplevel") raw_top_level = top_level_result.stdout.strip() @@ -610,27 +691,55 @@ class StepExecutor: print(f" Branch: {branch}") - def _commit_paths(self, paths: list[str], message: str) -> bool: + def _assert_step_scope(self, owned_paths: tuple[str, ...]) -> tuple[str, ...]: + allowed = {self._path_key(path) for path in owned_paths} + changed = self._worktree_paths() + unexpected = [ + path for path in changed + if not self._is_phase_owned_path(path) and self._path_key(path) not in allowed + ] + if unexpected: + raise StepOwnershipError("Step ownership violation: " + ", ".join(unexpected)) + try: + for path in owned_paths: + self._validate_owned_file(path) + except HarnessError as exc: + raise StepOwnershipError(str(exc)) from exc + return changed + + def _commit_paths( + self, paths: list[str], message: str, *, owned_paths: tuple[str, ...] = () + ) -> bool: + validated_paths = self._assert_step_scope(owned_paths) if not paths: return False # Codex may stage files even though the prompt tells it not to. Reset only # the index so the following path-scoped add controls the commit content; # working-tree data is preserved. This also expands staged renames back to # a deletion/addition pair, allowing both porcelain pathnames to be staged. - self._run_git_checked("reset", "--quiet", "HEAD", "--", ".") - self._run_git_checked("add", "-A", "--", *sorted(paths)) - diff = self._run_git("diff", "--cached", "--quiet") - if diff.returncode == 0: - return False - if diff.returncode != 1: - raise HarnessError( - f"git diff failed: {(diff.stderr or diff.stdout).strip()}" + if validated_paths: + self._run_git_checked( + "--literal-pathspecs", "reset", "--quiet", "HEAD", "--", *sorted(validated_paths) ) + self._run_git_checked("--literal-pathspecs", "add", "-A", "--", *sorted(paths)) + staged = self._run_git_checked( + "diff", "--cached", "--name-only", "--no-renames", "-z" + ).stdout + selected = {self._path_key(path) for path in paths} + unexpected = [ + path for path in staged.split("\0") + if path and self._path_key(path) not in selected + ] + if unexpected: + raise StepOwnershipError("Unexpected staged paths before commit: " + ", ".join(unexpected)) + self._assert_step_scope(owned_paths) + if not staged: + return False self._run_git_checked("commit", "-m", message) return True - def _commit_step(self, step_num: int, step_name: str): - changed = list(self._worktree_paths()) + def _commit_step(self, step_num: int, step_name: str, owned_paths: tuple[str, ...]): + changed = self._assert_step_scope(owned_paths) code_paths = [ path for path in changed @@ -639,18 +748,18 @@ class StepExecutor: message = self.FEAT_MSG.format( phase=self._phase_name, num=step_num, name=step_name ) - if self._commit_paths(code_paths, message): + if self._commit_paths(code_paths, message, owned_paths=owned_paths): print(f" Commit: {message}") metadata_paths = [ path - for path in self._worktree_paths() + for path in self._assert_step_scope(owned_paths) if self._is_phase_owned_path(path) and not self._is_step_output_path(path) ] message = self.CHORE_MSG.format( phase=self._phase_name, num=step_num ) - self._commit_paths(metadata_paths, message) + self._commit_paths(metadata_paths, message, owned_paths=owned_paths) # --- top-level index --- @@ -852,6 +961,7 @@ class StepExecutor: self._root, "-", ] + transport_failure = None try: result = subprocess.run( command, @@ -867,6 +977,7 @@ class StepExecutor: "Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요." ) from exc except subprocess.TimeoutExpired as exc: + transport_failure = "timeout" timeout_message = f"Codex timed out after {exc.timeout} seconds." stdout = self._process_text(exc.stdout) stderr = self._process_text(exc.stderr) @@ -887,6 +998,7 @@ class StepExecutor: "step": step_num, "name": step_name, "exitCode": result.returncode, "stdout": result.stdout, "stderr": result.stderr, + "transportFailure": transport_failure, } out_path = self._phase_dir / f"step{step_num}-output.json" self._write_json(out_path, output) @@ -989,9 +1101,69 @@ class StepExecutor: # --- 실행 루프 --- + def _classify_attempt( + self, current: dict, output: dict, environment_failure: Optional[str] + ) -> tuple[str, str, str]: + """Return local classification, action and reason; never infer retry from exit alone.""" + environment_failure = environment_failure or self._codex_environment_failure(output) + if environment_failure: + return "environment", "blocked", environment_failure + status = current["status"] + if status == "blocked": + return "reported-blocked", "blocked", current["blocked_reason"] + if status == "error": + return "reported-error", "error", current["error_message"] + exit_code = output["exitCode"] + if status == "completed": + if exit_code == 0: + return "completed", "completed", current["summary"] + return ( + "inconsistent-result", "error", + f"Step reported completed but Codex exited with code {exit_code}", + ) + diagnostic = (output.get("stderr", "").strip() or output.get("stdout", "").strip())[-4000:] + if output.get("transportFailure") == "timeout": + return "transport-timeout", "retry", diagnostic + if exit_code != 0: + return "process-error", "error", diagnostic or f"Codex exited with code {exit_code}" + return "missing-result", "error", "Step did not update its workflow status" + + def _record_attempt(self, current: dict, output: dict, classification: str, reason: str) -> None: + history = current.setdefault("attempt_history", []) + field = {"completed": "summary", "error": "error_message", "blocked": "blocked_reason"}.get(current["status"]) + history.append({ + "attempt": len(history) + 1, + "recorded_at": self._stamp(), + "exit_code": output["exitCode"], + "reported_status": current["status"], + "reported_reason": current.get(field, "") if field else "", + "classification": classification, + "reason": reason, + "diagnostic": (output.get("stderr", "").strip() or output.get("stdout", "").strip())[-4000:], + }) + + def _stop_step(self, index: dict, step_num: int, status: str, reason: str) -> None: + current = index["steps"][step_num] + current["attempt_history"][-1]["reason"] = reason + self._clear_step_outcome(current) + current["status"] = status + field, timestamp, exit_code = ( + ("blocked_reason", "blocked_at", 2) if status == "blocked" + else ("error_message", "failed_at", 1) + ) + current[field] = reason + current[timestamp] = self._stamp() + self._write_json(self._index_file, index) + self._update_top_index(status) + print(f" Step {step_num}: {status} — {reason}") + sys.exit(exit_code) + def _execute_single_step(self, step: dict, guardrails: str) -> bool: """Step을 실행한다. 자동 재시도는 Codex transport 실패에만 적용한다.""" step_num, step_name = step["step"], step["name"] + owned_paths = self._step_owned_paths(self._read_phase_index()["steps"][step_num]) + for path in owned_paths: + self._validate_owned_file(path) done = sum( 1 for item in self._read_phase_index()["steps"] @@ -1014,8 +1186,13 @@ class StepExecutor: f"{self.MAX_TRANSPORT_ATTEMPTS}]" ) + environment_failure = None with progress_indicator(tag) as pi: - output = self._invoke_codex(step, preamble) + try: + output = self._invoke_codex(step, preamble) + except CodexEnvironmentError as exc: + environment_failure = str(exc) + output = {"exitCode": None, "stdout": "", "stderr": str(exc)} elapsed = int(pi.elapsed) try: @@ -1040,7 +1217,7 @@ class StepExecutor: raise exit_code = output.get("exitCode") - if type(exit_code) is not int: + if type(exit_code) is not int and environment_failure is None: reason = "Codex output requires an integer exitCode" self._record_untrusted_step_result( index_before_invocation, @@ -1052,85 +1229,50 @@ class StepExecutor: raise HarnessError(reason) current = index["steps"][step_num] - status = current["status"] ts = self._stamp() + try: + self._assert_step_scope(owned_paths) + except StepOwnershipError as exc: + self._record_attempt(current, output, "ownership-violation", str(exc)) + self._stop_step(index, step_num, "error", str(exc)) + classification, action, reason = self._classify_attempt( + current, output, environment_failure + ) + self._record_attempt(current, output, classification, reason) - if exit_code != 0: - runtime_error = ( - output.get("stderr", "").strip() - or output.get("stdout", "").strip() - or f"Codex exited with code {exit_code}" - )[-4000:] - current["status"] = "pending" - self._clear_step_outcome(current) - self._write_json(self._index_file, index) - - environment_failure = self._codex_environment_failure(output) - if environment_failure: - raise CodexEnvironmentError(environment_failure) + if action == "retry": if attempt < self.MAX_TRANSPORT_ATTEMPTS: - prev_error = runtime_error + self._clear_step_outcome(current) + self._write_json(self._index_file, index) + prev_error = reason print( f" ↻ Step {step_num}: transport retry " - f"{attempt}/{self.MAX_TRANSPORT_ATTEMPTS} — {runtime_error}" + f"{attempt}/{self.MAX_TRANSPORT_ATTEMPTS} — {reason}" ) continue - - current["status"] = "error" - current["error_message"] = ( + reason = ( f"[transport failure after {self.MAX_TRANSPORT_ATTEMPTS} " - f"attempts] {runtime_error}" + f"attempts] {reason}" ) - current["failed_at"] = ts - self._write_json(self._index_file, index) - self._update_top_index("error") - print( - f" ✗ Step {step_num}: transport failed after " - f"{self.MAX_TRANSPORT_ATTEMPTS} attempts [{elapsed}s]" - ) - print(f" Error: {runtime_error}") - sys.exit(1) + action = "error" - if status == "completed": + if action == "completed": self._clear_step_outcome( current, frozenset({"summary", "completed_at"}) ) current["completed_at"] = ts self._write_json(self._index_file, index) - self._commit_step(step_num, step_name) + try: + self._commit_step(step_num, step_name, owned_paths) + except HarnessError as exc: + current["attempt_history"][-1]["classification"] = ( + "ownership-violation" if isinstance(exc, StepOwnershipError) else "commit-error" + ) + self._stop_step(index, step_num, "error", str(exc)) print(f" ✓ Step {step_num}: {step_name} [{elapsed}s]") return True - if status == "blocked": - self._clear_step_outcome( - current, frozenset({"blocked_reason", "blocked_at"}) - ) - current["blocked_at"] = ts - self._write_json(self._index_file, index) - print(f" ⏸ Step {step_num}: {step_name} blocked [{elapsed}s]") - print(f" Reason: {current['blocked_reason']}") - self._update_top_index("blocked") - sys.exit(2) - - if status == "error": - self._clear_step_outcome( - current, frozenset({"error_message", "failed_at"}) - ) - current["failed_at"] = ts - self._write_json(self._index_file, index) - self._update_top_index("error") - print(f" ✗ Step {step_num}: {step_name} failed [{elapsed}s]") - print(f" Error: {current['error_message']}") - sys.exit(1) - - current["status"] = "error" - self._clear_step_outcome(current) - current["error_message"] = "Step did not update its workflow status" - current["failed_at"] = ts - self._write_json(self._index_file, index) - self._update_top_index("error") - print(f" ✗ Step {step_num}: status was not updated [{elapsed}s]") - sys.exit(1) + self._stop_step(index, step_num, action, reason) return False # unreachable diff --git a/tests/test_agent_skill_workflow_contract.py b/tests/test_agent_skill_workflow_contract.py index 20c5c27..3fa9241 100644 --- a/tests/test_agent_skill_workflow_contract.py +++ b/tests/test_agent_skill_workflow_contract.py @@ -3,6 +3,10 @@ from pathlib import Path import re import tomllib +import pytest + +from test_execute import step_runtime + ROOT = Path(__file__).resolve().parents[1] AGENT_DIR = ROOT / ".codex" / "agents" @@ -24,14 +28,14 @@ EXPECTED_AGENTS = { EXPECTED_SKILLS = { "fem-theory-query", - "fesa-cpp-msvc-tdd", - "fesa-formulation-spec", - "fesa-io-contract", - "fesa-numerical-review", - "fesa-physics-sanity", - "fesa-release-readiness", - "fesa-requirements-baseline", - "fesa-research-evidence", + "fem-implementation-tdd", + "fem-formulation-spec", + "fem-io-contract", + "fem-numerical-review", + "fem-physics-review", + "fem-release-readiness", + "fem-requirements-baseline", + "fem-research-evidence", } LEGACY_OUTPUT_DIRS = { @@ -94,22 +98,27 @@ AGENT_STAGE_GUIDANCE_FILES = ( ) EXPECTED_AGENT_STAGE_MAPPING = ( - ("1. 요구조건", "requirement-agent", "requirements.md"), - ("2. 연구", "research-agent", "research.md"), - ("3. 정식화", "formulation-agent", "formulation.md"), + ("requirements", "requirement-agent", ("requirements.md",)), + ("research", "research-agent", ("research.md",)), + ("formulation", "formulation-agent", ("formulation.md",)), ( - "4. 수치 검토 + reference model 계약", + "numerical-review", "numerical-review-agent", - "numerical-review.md, reference-model.md", + ("numerical-review.md", "reference-model.md"), ), - ("5. I/O 정의", "io-definition-agent", "io.md"), + ("io-definition", "io-definition-agent", ("io.md",)), ( - "6. 구현 계획 + C++ 구현 + build/test + reference comparison", - "implementation-planning-agent, implementation-agent", - "implementation-plan.md, implementation-report.md, build-test.md, reference-comparison.md", + "implementation-planning", + "implementation-planning-agent", + ("implementation-plan.md",), ), - ("7. 물리 검토", "physics-evaluation-agent", "physics-evaluation.md"), - ("8. 배포 준비", "release-agent", "release.md"), + ( + "implementation", + "implementation-agent", + ("implementation-report.md", "build-test.md", "reference-comparison.md"), + ), + ("physics-evaluation", "physics-evaluation-agent", ("physics-evaluation.md",)), + ("release-readiness", "release-agent", ("release.md",)), ) CANONICAL_FEATURE_DIRS = ( @@ -173,12 +182,24 @@ def live_contract_text() -> str: return "\n".join(read(path) for path in paths) -def markdown_stage_mapping(path: Path) -> tuple[tuple[str, str, str], ...]: +def markdown_stage_mapping(path: Path) -> tuple[tuple[str, str, tuple[str, ...]], ...]: + heading = ( + "## Agent/Skill Workflow" + if path.name == "AGENTS.md" + else "## 4. 단계와 소유권" + ) + section = read(path).split(heading, maxsplit=1)[1].split("\n## ", maxsplit=1)[0] rows = [] - for line in read(path).splitlines(): + for line in section.splitlines(): + if not line.lstrip().startswith("|"): + continue columns = [column.strip().replace("`", "") for column in line.strip().split("|")[1:-1]] - if len(columns) >= 4 and re.fullmatch(r"[1-8]\. .+", columns[0]): - rows.append((columns[0], columns[1], columns[3])) + if len(columns) < 3: + continue + stage = re.sub(r"^\d+\.\s+", "", columns[0]) + if re.fullmatch(r"[a-z]+(?:-[a-z]+)*", stage): + reports = tuple(re.findall(r"\b[\w-]+\.md\b", columns[-1])) + rows.append((stage, columns[1], reports)) return tuple(rows) @@ -254,84 +275,68 @@ def test_existing_feature_artifacts_are_bundled(): for feature_id, expected_files in FEATURE_FILES.items(): feature_dir = docs_dir / feature_id actual_files = {path.name for path in feature_dir.glob("*.md")} - assert actual_files == expected_files + # Preserve historical artifacts while allowing Feature management and linked details. + assert expected_files <= actual_files def test_numerical_review_absorbs_reference_model_contract(): assert not (AGENT_DIR / "reference-model-agent.toml").exists() assert not (SKILL_DIR / "fesa-reference-models").exists() - text = read(SKILL_DIR / "fesa-numerical-review" / "SKILL.md").lower() - for marker in ( - "numerical-review.md", - "reference-model.md", - "reference case inventory", - "source identity", - "row precheck", - "tolerance", - "pass-for-io-definition", - ): - assert marker in text + text = read(SKILL_DIR / "fem-numerical-review" / "SKILL.md") + outputs = set(re.findall(r"docs//([\w-]+\.md)", text)) + assert {"numerical-review.md", "reference-model.md"} <= outputs def test_implementation_absorbs_build_test_and_reference_comparison(): assert not (AGENT_DIR / "build-test-executor-agent.toml").exists() assert not (AGENT_DIR / "reference-verification-agent.toml").exists() assert not (SKILL_DIR / "fesa-reference-comparison").exists() - text = read(SKILL_DIR / "fesa-cpp-msvc-tdd" / "SKILL.md").lower() - for marker in ( + text = read(SKILL_DIR / "fem-implementation-tdd" / "SKILL.md") + outputs = set(re.findall(r"docs//([\w-]+\.md)", text)) + assert { + "implementation-plan.md", "implementation-report.md", "build-test.md", "reference-comparison.md", - "artifact check -> compare -> classify -> report", - "pass-for-physics-evaluation", - ): - assert marker in text + "corrections.md", + } <= outputs -def test_implementation_steps_keep_final_comparison_in_a_final_gate(): - paths = ( - AGENT_DIR / "implementation-agent.toml", - SKILL_DIR / "fesa-cpp-msvc-tdd" / "SKILL.md", - ) - for path in paths: - text = read(path).lower() - assert "final implementation-owned verification step/gate" in text, path - assert "non-final step" in text, path - assert "artifact check -> compare -> classify -> report" in text, path - assert "results.h5" in text, path - assert "reference-comparison.md" in text, path +def test_implementation_verification_reference_is_resolvable(): + skill = SKILL_DIR / "fem-implementation-tdd" / "SKILL.md" + links = re.findall(r"\[[^\]]+\]\(([^)]+)\)", read(skill)) + assert "references/toolchain-and-verification.md" in links + for link in links: + assert (skill.parent / link).is_file(), link -def test_p_agent_001_implementation_agent_reads_coding_style_before_cpp_steps(): +def test_p_agent_001_implementation_guidance_resolves_coding_style_contract(): parsed = tomllib.loads(read(AGENT_DIR / "implementation-agent.toml")) instructions = parsed["developer_instructions"] - section_match = re.search( - r"Mandatory global input:\n(?P
.*?)(?=\n[A-Z][^\n]+:\n)", - instructions, - re.DOTALL, - ) - - assert section_match is not None - section = section_match.group("section") - assert "Before every C++ implementation Step" in section - assert "read docs/CODINGSTYLE.md" in section - assert "apply it to production and test code" in section - assert "Doxygen coverage applies only to production code" in section + assert "AGENTS.md" in instructions + project_links = re.findall(r"\[[^\]]+\]\(([^)]+)\)", read(ROOT / "AGENTS.md")) + assert "docs/CODINGSTYLE.md" in project_links + assert (ROOT / "docs/CODINGSTYLE.md").is_file() -def test_agent_hierarchy_is_explicit(): - coordinator = read(AGENT_DIR / "coordinator-agent.toml").lower() - assert "main agent" in coordinator - assert "docs//coordination.md" in coordinator - assert "worklist" in coordinator - assert "sub-agent dispatch" in coordinator - +def test_agent_profiles_reference_shared_authority_and_feature_context(): for path in sorted(AGENT_DIR.glob("*.toml")): - if path.name == "coordinator-agent.toml": - continue - text = read(path).lower() - assert "sub-agent" in text, path.name - assert "coordinator agent" in text, path.name + instructions = tomllib.loads(read(path))["developer_instructions"] + for document in ( + "AGENTS.md", + "docs/SOLVER_AGENT_DESIGN.md", + "docs/FEATURE_DOCUMENTATION.md", + "FEATURED_AGENTS.md", + "PRD.md", + "ADR.md", + ): + assert document in instructions, (path.name, document) + for skill_name in re.findall(r"\$([a-z0-9-]+)", instructions): + candidates = ( + SKILL_DIR / skill_name / "SKILL.md", + ROOT / ".agents/skills" / skill_name / "SKILL.md", + ) + assert any(candidate.is_file() for candidate in candidates), (path, skill_name) def test_live_contracts_have_no_retired_names_or_paths(): @@ -341,10 +346,9 @@ def test_live_contracts_have_no_retired_names_or_paths(): assert token not in text, token -def test_live_guidance_declares_exact_ordered_eight_stage_workflow(): +def test_live_guidance_declares_consistent_ordered_nine_stage_workflow(): for path in LIVE_CONTRACT_FILES: text = read(path) - assert "8단계" in text, path assert "docs//" in text, path for path in AGENT_STAGE_GUIDANCE_FILES: @@ -389,7 +393,9 @@ def test_agent_toml_and_limited_skill_yaml_schemas_are_parseable(): for path in sorted(AGENT_DIR.glob("*.toml")): parsed = tomllib.loads(read(path)) assert parsed["name"] == path.stem - assert parsed["model_reasoning_effort"] == "extra high" + assert parsed["model_reasoning_effort"] == "xhigh" + assert parsed["sandbox_mode"] == "workspace-write" + assert parsed["developer_instructions"].strip(), path for skill_name in EXPECTED_SKILLS: skill_dir = SKILL_DIR / skill_name @@ -397,3 +403,20 @@ def test_agent_toml_and_limited_skill_yaml_schemas_are_parseable(): interface = parse_skill_interface(skill_dir / "agents" / "openai.yaml") assert frontmatter["name"] == skill_name assert f"${skill_name}" in interface["default_prompt"] + + +@pytest.mark.parametrize("document", [".agents/skills/harness/SKILL.md", "docs/HARNESS_WORKFLOW.md"]) +def test_documented_phase_example_passes_executor_preflight(step_runtime, document): + """Exercise the documented schema with its real consumer, not prose matching.""" + from scripts import execute + + examples = [json.loads(block) for block in re.findall(r"```json\s*\n(.*?)\n```", read(ROOT / document), re.DOTALL)] + phase = next(example for example in examples if "steps" in example) + phase["project"] = "Harness Tests" + phase["phase"] = "runtime" + phase["steps"][0]["name"] = "runtime" + runtime = step_runtime + runtime.executor._write_json(runtime.index_file, phase) + executor = execute.StepExecutor("runtime") + executor._preflight_repository() + assert runtime.calls == [] diff --git a/tests/test_execute.py b/tests/test_execute.py index 157f9c3..d2a75e3 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -1,18 +1,33 @@ import json +import os import subprocess +from types import SimpleNamespace import pytest from scripts import execute +@pytest.fixture(autouse=True) +def clear_sandbox_override(monkeypatch): + monkeypatch.delenv("FEM_HARNESS_CODEX_SANDBOX", raising=False) + + +def write_top_index(phase_dir): + (phase_dir.parent / "index.json").write_text( + json.dumps({"phases": [{"dir": phase_dir.name, "status": "pending"}]}), + encoding="utf-8", + ) + + def write_implementation_agent_profile( root, *, model="gpt-5.6-terra", reasoning="xhigh" ): profile = root / ".codex" / "agents" / "implementation-agent.toml" profile.parent.mkdir(parents=True) profile.write_text( - f'model = "{model}"\nmodel_reasoning_effort = "{reasoning}"\n', + f'model = "{model}"\nmodel_reasoning_effort = "{reasoning}"\n' + 'developer_instructions = """Implementation Agent test role."""\n', encoding="utf-8", ) @@ -20,6 +35,7 @@ def write_implementation_agent_profile( def test_invoke_codex_uses_utf8_for_unicode_prompt(tmp_path, monkeypatch): phase_dir = tmp_path / "phases" / "unicode-phase" phase_dir.mkdir(parents=True) + write_top_index(phase_dir) (phase_dir / "index.json").write_text( json.dumps( { @@ -57,6 +73,7 @@ def test_invoke_codex_uses_implementation_agent_command_contract( ): phase_dir = tmp_path / "phases" / "agent-phase" phase_dir.mkdir(parents=True) + write_top_index(phase_dir) (phase_dir / "index.json").write_text( json.dumps( { @@ -95,6 +112,7 @@ def test_invoke_codex_accepts_explicit_danger_full_access_override( ): phase_dir = tmp_path / "phases" / "sandbox-phase" phase_dir.mkdir(parents=True) + write_top_index(phase_dir) (phase_dir / "index.json").write_text( json.dumps( { @@ -111,14 +129,23 @@ def test_invoke_codex_accepts_explicit_danger_full_access_override( invocation = {} def capture_run(command, **kwargs): + if command[0] == "git": + responses = { + ("rev-parse", "--absolute-git-dir"): str(tmp_path / ".git/worktrees/sandbox"), + ("rev-parse", "--git-common-dir"): str(tmp_path / ".git"), + ("status", "--porcelain=v1", "-z", "--untracked-files=all"): "", + } + return subprocess.CompletedProcess( + command, 0, stdout=responses[tuple(command[1:])], stderr="" + ) invocation["command"] = command return subprocess.CompletedProcess(command, 0, stdout="", stderr="") monkeypatch.setattr(execute, "ROOT", tmp_path) monkeypatch.setattr(execute.subprocess, "run", capture_run) - monkeypatch.setenv("FESA_HARNESS_CODEX_SANDBOX", "danger-full-access") + monkeypatch.setenv("FEM_HARNESS_CODEX_SANDBOX", "danger-full-access") - executor = execute.StepExecutor("sandbox-phase") + executor = execute.StepExecutor("sandbox-phase", danger_full_access_approved=True) executor._invoke_codex({"step": 0, "name": "sandbox"}, "") sandbox_index = invocation["command"].index("--sandbox") @@ -128,6 +155,7 @@ def test_invoke_codex_accepts_explicit_danger_full_access_override( def test_invoke_codex_timeout_writes_structured_failure(tmp_path, monkeypatch): phase_dir = tmp_path / "phases" / "timeout-phase" phase_dir.mkdir(parents=True) + write_top_index(phase_dir) (phase_dir / "index.json").write_text( json.dumps( { @@ -153,6 +181,7 @@ def test_invoke_codex_timeout_writes_structured_failure(tmp_path, monkeypatch): output = executor._invoke_codex({"step": 0, "name": "timeout"}, "") assert output["exitCode"] == 124 + assert output["transportFailure"] == "timeout" assert output["stdout"] == "partial stdout" assert "Codex timed out after 1800 seconds." in output["stderr"] stored = json.loads((phase_dir / "step0-output.json").read_text(encoding="utf-8")) @@ -162,12 +191,13 @@ def test_invoke_codex_timeout_writes_structured_failure(tmp_path, monkeypatch): def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypatch): phase_dir = tmp_path / "phases" / "retry-phase" phase_dir.mkdir(parents=True) + write_top_index(phase_dir) (phase_dir / "index.json").write_text( json.dumps( { "project": "FESA Structural Solver", "phase": "retry-phase", - "steps": [{"step": 0, "name": "retry", "status": "pending"}], + "steps": [{"step": 0, "name": "retry", "status": "pending", "owned_paths": []}], } ), encoding="utf-8", @@ -177,6 +207,15 @@ def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypa attempts = [] def timeout_run(command, **kwargs): + if command[0] == "git": + responses = { + ("symbolic-ref", "--quiet", "HEAD"): "refs/heads/feat-retry-phase", + ("rev-parse", "--verify", "HEAD"): "a" * 40, + ("status", "--porcelain=v1", "-z", "--untracked-files=all"): "", + } + return subprocess.CompletedProcess( + command, 0, stdout=responses[tuple(command[1:])], stderr="" + ) attempts.append(command) raise subprocess.TimeoutExpired(command, kwargs["timeout"]) @@ -184,9 +223,6 @@ def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypa monkeypatch.setattr(execute.subprocess, "run", timeout_run) executor = execute.StepExecutor("retry-phase") - executor.MAX_RETRIES = 2 - monkeypatch.setattr(executor, "_commit_step", lambda *args: None) - monkeypatch.setattr(executor, "_update_top_index", lambda *args: None) with pytest.raises(SystemExit, match="1"): executor._execute_single_step( @@ -197,11 +233,15 @@ def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypa index = json.loads((phase_dir / "index.json").read_text(encoding="utf-8")) assert index["steps"][0]["status"] == "error" assert "Codex timed out after 1800 seconds." in index["steps"][0]["error_message"] + assert index["steps"][0]["attempt_history"][-1]["reason"] == index["steps"][0]["error_message"] + top = json.loads((phase_dir.parent / "index.json").read_text(encoding="utf-8")) + assert top["phases"][0]["status"] == "error" def test_invoke_codex_rejects_unknown_sandbox_override(tmp_path, monkeypatch): phase_dir = tmp_path / "phases" / "sandbox-phase" phase_dir.mkdir(parents=True) + write_top_index(phase_dir) (phase_dir / "index.json").write_text( json.dumps( { @@ -215,13 +255,59 @@ def test_invoke_codex_rejects_unknown_sandbox_override(tmp_path, monkeypatch): (phase_dir / "step0.md").write_text("sandbox override", encoding="utf-8") monkeypatch.setattr(execute, "ROOT", tmp_path) - monkeypatch.setenv("FESA_HARNESS_CODEX_SANDBOX", "read-only") + monkeypatch.setenv("FEM_HARNESS_CODEX_SANDBOX", "read-only") executor = execute.StepExecutor("sandbox-phase") - with pytest.raises(execute.CodexEnvironmentError, match="FESA_HARNESS_CODEX_SANDBOX"): + with pytest.raises(execute.CodexEnvironmentError, match="FEM_HARNESS_CODEX_SANDBOX"): executor._invoke_codex({"step": 0, "name": "sandbox"}, "") +@pytest.mark.parametrize( + ("approved", "isolated", "porcelain", "expected_error"), + [ + (False, True, "", "--approve-danger-full-access"), + (True, False, "", "isolated linked worktree"), + (True, True, " M unrelated.md\0", "clean worktree"), + ], +) +def test_sandbox_elevation_rejects_missing_safety_conditions( + tmp_path, monkeypatch, approved, isolated, porcelain, expected_error +): + phase_dir = tmp_path / "phases" / "sandbox-phase" + phase_dir.mkdir(parents=True) + write_top_index(phase_dir) + (phase_dir / "step0.md").write_text("sandbox guard", encoding="utf-8") + (phase_dir / "index.json").write_text( + json.dumps({ + "project": "FESA Structural Solver", + "phase": "sandbox-phase", + "steps": [{"step": 0, "name": "sandbox", "status": "pending"}], + }), + encoding="utf-8", + ) + + def git_probe(command, **kwargs): + assert command[0] == "git", "Rejected elevation must not launch Codex" + common_dir = tmp_path / ".git" + git_dir = common_dir / "worktrees/sandbox" if isolated else common_dir + responses = { + ("rev-parse", "--absolute-git-dir"): str(git_dir), + ("rev-parse", "--git-common-dir"): str(common_dir), + ("status", "--porcelain=v1", "-z", "--untracked-files=all"): porcelain, + } + return subprocess.CompletedProcess( + command, 0, stdout=responses[tuple(command[1:])], stderr="" + ) + + monkeypatch.setattr(execute, "ROOT", tmp_path) + monkeypatch.setattr(execute.subprocess, "run", git_probe) + monkeypatch.setenv("FEM_HARNESS_CODEX_SANDBOX", "danger-full-access") + executor = execute.StepExecutor("sandbox-phase", danger_full_access_approved=approved) + + with pytest.raises(execute.CodexEnvironmentError, match=expected_error): + executor._codex_sandbox_mode() + + def test_codex_environment_failure_detects_missing_sandbox_helper_on_zero_exit(): output = { "exitCode": 0, @@ -279,3 +365,205 @@ def test_configure_utf8_stdio_reconfigures_both_streams(monkeypatch): assert stdout.encodings == ["utf-8"] assert stderr.encodings == ["utf-8"] + + +@pytest.fixture +def step_runtime(tmp_path, monkeypatch): + """Run the real Step loop and Git against a disposable repo; replace only Codex.""" + monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull) + monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1") + monkeypatch.delenv("FEM_HARNESS_CODEX_SANDBOX", raising=False) + real_run = subprocess.run + + def git(*args): + return real_run( + ["git", *args], cwd=tmp_path, capture_output=True, + text=True, encoding="utf-8", check=True, + ).stdout + + git("init", "--quiet") + git("config", "user.name", "Harness Tests") + git("config", "user.email", "harness@example.invalid") + git("config", "commit.gpgsign", "false") + empty_hooks = tmp_path / ".git" / "empty-hooks" + empty_hooks.mkdir() + git("config", "core.hooksPath", str(empty_hooks)) + phase_dir = tmp_path / "phases" / "runtime" + phase_dir.mkdir(parents=True) + write_top_index(phase_dir) + index_file = phase_dir / "index.json" + index_file.write_text(json.dumps({ + "project": "Harness Tests", "phase": "runtime", + "steps": [{"step": 0, "name": "runtime", "status": "pending", "owned_paths": []}], + }), encoding="utf-8") + (phase_dir / "step0.md").write_text("Run the assigned test Step.", encoding="utf-8") + (tmp_path / ".gitignore").write_text("phases/**/step*-output.json\n", encoding="utf-8") + write_implementation_agent_profile(tmp_path) + git("add", "--", ".") + git("commit", "--quiet", "-m", "test: initialize disposable repo") + monkeypatch.setattr(execute, "ROOT", tmp_path) + runtime = SimpleNamespace( + executor=execute.StepExecutor("runtime"), root=tmp_path, + index_file=index_file, git=git, real_run=real_run, responses=[], calls=[], + ) + + def invoke(command, **kwargs): + if command[0] == "git": + return real_run(command, **kwargs) + assert command[0] == "codex", "Only the simulated Codex process is allowed" + runtime.calls.append(kwargs["input"]) + response = runtime.responses.pop(0) + if callable(response): + response = response(runtime) + if isinstance(response, Exception): + raise response + index = json.loads(index_file.read_text(encoding="utf-8")) + current = index["steps"][0] + current["status"] = response.get("status", "pending") + if current["status"] != "pending": + field = {"completed": "summary", "error": "error_message", "blocked": "blocked_reason"}[current["status"]] + current[field] = response.get("reason", "reported reason") + index_file.write_text(json.dumps(index), encoding="utf-8") + return subprocess.CompletedProcess( + command, response.get("exit", 0), + stdout=response.get("stdout", ""), stderr=response.get("stderr", ""), + ) + + monkeypatch.setattr(execute.subprocess, "run", invoke) + return runtime + + +def run_step(runtime): + index = runtime.executor._read_phase_index() + return runtime.executor._execute_single_step(index["steps"][0], "") + + +@pytest.mark.parametrize("status,expected_exit", [("error", 1), ("blocked", 2)]) +@pytest.mark.parametrize("process_exit", [0, 1, 124]) +def test_explicit_failure_preserves_reason_without_retry(step_runtime, status, expected_exit, process_exit): + runtime = step_runtime + runtime.responses = [ + {"status": status, "reason": "original failure", "exit": process_exit, "stderr": "process diagnostic"}, + {"status": "completed"}, + ] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == expected_exit + assert len(runtime.calls) == 1 + current = runtime.executor._read_phase_index()["steps"][0] + assert current["status"] == status + assert current["error_message" if status == "error" else "blocked_reason"] == "original failure" + assert current["attempt_history"][0]["reported_reason"] == "original failure" + assert current["attempt_history"][0]["reason"] == "original failure" + assert current["attempt_history"][0]["exit_code"] == process_exit + assert current["attempt_history"][0]["diagnostic"] == "process diagnostic" + assert runtime.executor._read_top_index()["phases"][0]["status"] == status + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1" + + +@pytest.mark.parametrize("process_exit", [0, 1]) +@pytest.mark.parametrize("status", ["pending", "completed", "error"]) +def test_environment_failure_blocks_independent_of_exit_and_report(step_runtime, process_exit, status): + runtime = step_runtime + diagnostic = "windows sandbox: orchestrator_helper_launch_failed: helper not found" + runtime.responses = [{"status": status, "exit": process_exit, "stderr": diagnostic}] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 2 + assert len(runtime.calls) == 1 + current = runtime.executor._read_phase_index()["steps"][0] + assert current["status"] == "blocked" + assert "sandbox helper" in current["blocked_reason"] + record = current["attempt_history"][0] + assert record["reported_status"] == status + assert record["classification"] == "environment" + assert record["diagnostic"] == diagnostic + assert runtime.executor._read_top_index()["phases"][0]["status"] == "blocked" + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1" + + +def test_missing_cli_is_persisted_as_environment_block(step_runtime): + runtime = step_runtime + runtime.responses = [FileNotFoundError("codex")] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 2 + current = runtime.executor._read_phase_index()["steps"][0] + assert current["status"] == "blocked" + assert current["attempt_history"][0]["exit_code"] is None + assert runtime.executor._read_top_index()["phases"][0]["status"] == "blocked" + assert len(runtime.calls) == 1 + + +@pytest.mark.parametrize("status,process_exit", [("pending", 0), ("pending", 1), ("pending", 124), ("completed", 1)]) +def test_unclassified_or_inconsistent_result_never_retries(step_runtime, status, process_exit): + runtime = step_runtime + runtime.responses = [{"status": status, "exit": process_exit}, {"status": "completed"}] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 1 + assert len(runtime.calls) == 1 + assert runtime.executor._read_phase_index()["steps"][0]["status"] == "error" + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1" + + +def test_timeout_retry_preserves_history_and_can_complete(step_runtime): + runtime = step_runtime + runtime.responses = [subprocess.TimeoutExpired("codex", 1800), {"status": "completed"}] + assert run_step(runtime) + current = runtime.executor._read_phase_index()["steps"][0] + assert current["status"] == "completed" + assert [record["classification"] for record in current["attempt_history"]] == ["transport-timeout", "completed"] + assert [record["attempt"] for record in current["attempt_history"]] == [1, 2] + assert "timed out" in runtime.calls[1] + assert runtime.git("status", "--porcelain").strip() == "" + + +def test_history_survives_assigned_pending_reset(step_runtime): + runtime = step_runtime + runtime.responses = [{"status": "error", "reason": "original failure"}] + with pytest.raises(SystemExit): + run_step(runtime) + index = runtime.executor._read_phase_index() + history = index["steps"][0]["attempt_history"] + index["steps"][0]["status"] = "pending" + runtime.executor._write_json(runtime.index_file, index) + runtime.executor._ensure_created_at() + current = runtime.executor._read_phase_index()["steps"][0] + assert "error_message" not in current + assert current["attempt_history"] == history + runtime.responses = [{"status": "completed"}] + assert run_step(runtime) + assert [record["attempt"] for record in runtime.executor._read_phase_index()["steps"][0]["attempt_history"]] == [1, 2] + + +def test_full_runner_completes_metadata_only_phase(step_runtime): + runtime = step_runtime + runtime.responses = [{"status": "completed"}] + runtime.executor.run() + assert len(runtime.calls) == 1 + assert runtime.executor._read_top_index()["phases"][0]["status"] == "completed" + assert "completed_at" in runtime.executor._read_phase_index() + assert runtime.git("symbolic-ref", "HEAD").strip() == "refs/heads/feat-runtime" + assert runtime.git("status", "--porcelain").strip() == "" + + +@pytest.mark.parametrize("status", ["error", "blocked", "completed"]) +def test_terminal_report_prevents_retry_even_when_wrapper_times_out(step_runtime, status): + runtime = step_runtime + + def report_then_timeout(runtime): + index = runtime.executor._read_phase_index() + current = index["steps"][0] + current["status"] = status + field = {"error": "error_message", "blocked": "blocked_reason", "completed": "summary"}[status] + current[field] = "original terminal report" + runtime.executor._write_json(runtime.index_file, index) + return subprocess.TimeoutExpired("codex", 1800) + + runtime.responses = [report_then_timeout] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == (2 if status == "blocked" else 1) + assert len(runtime.calls) == 1 + assert runtime.executor._read_phase_index()["steps"][0]["attempt_history"][0]["reported_reason"] == "original terminal report" diff --git a/tests/test_execute_ownership.py b/tests/test_execute_ownership.py new file mode 100644 index 0000000..1f1f307 --- /dev/null +++ b/tests/test_execute_ownership.py @@ -0,0 +1,348 @@ +import os +import subprocess + +import pytest + +from scripts import execute +from test_execute import run_step, step_runtime + + +def set_owned_paths(runtime, paths): + index = runtime.executor._read_phase_index() + index["steps"][0]["owned_paths"] = paths + runtime.executor._write_json(runtime.index_file, index) + + +def link_directory(runtime, link, target): + if os.name == "nt": + # Junctions exercise Windows link resolution without symlink privileges. + runtime.real_run( + ["cmd", "/c", "mklink", "/J", str(link), str(target)], + cwd=runtime.root, capture_output=True, check=True, + ) + else: + link.symlink_to(target, target_is_directory=True) + + +@pytest.mark.parametrize("paths", [ + None, "src/a.py", [1], [""], ["."], ["src/"], ["src/**"], + ["../a.py"], ["src/../a.py"], ["/a.py"], ["C:/a.py"], ["C:a.py"], + ["\\\\server\\share\\a.py"], ["src/a.py:stream"], [".git/config"], + ["phases/runtime/index.json"], ["phases/other/step0.md"], + ["src/a.py", "src\\a.py"], ["src/a.py\x00"], ["src/a.py."], +]) +def test_owned_paths_rejects_ambiguous_or_unsafe_entries(step_runtime, paths): + runtime = step_runtime + set_owned_paths(runtime, paths) + with pytest.raises(execute.HarnessError, match="owned_paths"): + runtime.executor._read_phase_index() + + +@pytest.mark.skipif(os.name != "nt", reason="Windows path identity") +def test_owned_paths_rejects_case_alias_duplicates(step_runtime): + set_owned_paths(step_runtime, ["src/a.py", "SRC/A.PY"]) + with pytest.raises(execute.HarnessError, match="owned_paths"): + step_runtime.executor._read_phase_index() + + +def test_missing_pending_ownership_stops_before_branch_or_state_changes(step_runtime): + runtime = step_runtime + index = runtime.executor._read_phase_index() + del index["steps"][0]["owned_paths"] + runtime.executor._write_json(runtime.index_file, index) + original = runtime.index_file.read_bytes() + branch = runtime.git("symbolic-ref", "HEAD") + with pytest.raises(execute.HarnessError, match="owned_paths"): + runtime.executor.run() + assert runtime.calls == [] + assert runtime.index_file.read_bytes() == original + assert runtime.git("symbolic-ref", "HEAD") == branch + + +def test_completed_history_without_ownership_remains_readable(step_runtime): + runtime = step_runtime + index = runtime.executor._read_phase_index() + index["steps"][0] = {"step": 0, "name": "runtime", "status": "completed", "summary": "historical"} + runtime.executor._write_json(runtime.index_file, index) + assert execute.StepExecutor("runtime")._read_phase_index()["steps"][0]["summary"] == "historical" + + +@pytest.mark.parametrize("path", ["src", "escape/file.py"]) +def test_pending_ownership_rejects_directory_or_link_escape(step_runtime, path): + runtime = step_runtime + if path == "src": + (runtime.root / "src").mkdir() + else: + outside = runtime.root.parent / (runtime.root.name + "-outside") + outside.mkdir() + link_directory(runtime, runtime.root / "escape", outside) + set_owned_paths(runtime, [path]) + with pytest.raises(execute.HarnessError, match="owned_paths"): + runtime.executor._preflight_repository() + assert runtime.calls == [] + + +@pytest.mark.parametrize("history", [None, {}, ["bad"], [{"attempt": 1}]]) +def test_malformed_attempt_history_is_rejected_without_erasure(step_runtime, history): + runtime = step_runtime + index = runtime.executor._read_phase_index() + index["steps"][0]["attempt_history"] = history + runtime.executor._write_json(runtime.index_file, index) + original = runtime.index_file.read_bytes() + with pytest.raises(execute.HarnessError, match="attempt_history"): + runtime.executor._read_phase_index() + assert runtime.index_file.read_bytes() == original + + +@pytest.mark.parametrize("field,value", [("owned_paths", ["unassigned.md"]), ("attempt_history", [])]) +def test_agent_cannot_change_executor_or_planning_metadata(step_runtime, field, value): + runtime = step_runtime + + def tamper(runtime): + index = runtime.executor._read_phase_index() + index["steps"][0][field] = value + runtime.executor._write_json(runtime.index_file, index) + return {"status": "completed"} + + runtime.responses = [tamper] + with pytest.raises(execute.HarnessError, match="immutable metadata"): + run_step(runtime) + current = runtime.executor._read_phase_index()["steps"][0] + assert current["owned_paths"] == [] + assert "attempt_history" not in current + assert current["status"] == "error" + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1" + + +@pytest.mark.parametrize("field,value", [("reported_status", []), ("classification", []), ("exit_code", True), ("attempt", True)]) +def test_history_record_types_fail_closed(step_runtime, field, value): + runtime = step_runtime + runtime.responses = [{"status": "error"}] + with pytest.raises(SystemExit): + run_step(runtime) + index = runtime.executor._read_phase_index() + index["steps"][0]["attempt_history"][0][field] = value + runtime.executor._write_json(runtime.index_file, index) + with pytest.raises(execute.HarnessError, match="attempt_history"): + runtime.executor._read_phase_index() + + +def test_all_pending_steps_need_ownership_before_execution(step_runtime): + runtime = step_runtime + index = runtime.executor._read_phase_index() + index["steps"].append({"step": 1, "name": "later", "status": "pending"}) + (runtime.index_file.parent / "step1.md").write_text("Later Step", encoding="utf-8") + runtime.executor._write_json(runtime.index_file, index) + executor = execute.StepExecutor("runtime") + with pytest.raises(execute.HarnessError, match="Step 1 requires an owned_paths"): + executor.run() + assert runtime.calls == [] + assert "created_at" not in executor._read_phase_index() + + +def test_metadata_commit_failure_preserves_implementation_commit_and_reason(step_runtime, monkeypatch): + runtime = step_runtime + set_owned_paths(runtime, ["approved.py"]) + + def change(runtime): + (runtime.root / "approved.py").write_text("approved\n", encoding="utf-8") + return {"status": "completed"} + + original_run_git = runtime.executor._run_git + + def fail_metadata_commit(*args): + if args[:2] == ("commit", "-m") and args[-1].startswith("chore("): + return subprocess.CompletedProcess(args, 1, stdout="", stderr="metadata commit failed") + return original_run_git(*args) + + monkeypatch.setattr(runtime.executor, "_run_git", fail_metadata_commit) + runtime.responses = [change] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 1 + current = runtime.executor._read_phase_index()["steps"][0] + assert current["status"] == "error" + assert current["attempt_history"][-1]["reason"] == current["error_message"] + assert current["attempt_history"][-1]["classification"] == "commit-error" + assert runtime.git("rev-list", "--count", "HEAD").strip() == "2" + assert runtime.git("show", "HEAD:approved.py") == "approved\n" + assert runtime.executor._read_top_index()["phases"][0]["status"] == "error" + + +@pytest.mark.parametrize("staged", [False, True]) +@pytest.mark.parametrize("result", ["completed", "timeout"]) +def test_unassigned_changes_stop_commit_and_retry_without_reverting_files(step_runtime, staged, result): + runtime = step_runtime + set_owned_paths(runtime, ["approved.py"]) + + def change(runtime): + (runtime.root / "approved.py").write_text("approved change\n", encoding="utf-8") + (runtime.root / "unassigned.md").write_text("user change\n", encoding="utf-8") + if staged: + runtime.git("add", "--", "unassigned.md") + if result == "timeout": + return subprocess.TimeoutExpired("codex", 1800) + return {"status": "completed"} + + runtime.responses = [change, {"status": "completed"}] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 1 + assert len(runtime.calls) == 1 + current = runtime.executor._read_phase_index()["steps"][0] + assert current["status"] == "error" + assert "unassigned.md" in current["error_message"] + assert current["attempt_history"][0]["classification"] == "ownership-violation" + assert "unassigned.md" in current["attempt_history"][0]["reason"] + assert (runtime.root / "unassigned.md").read_text(encoding="utf-8") == "user change\n" + assert (runtime.root / "approved.py").read_text(encoding="utf-8") == "approved change\n" + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1" + assert runtime.git("diff", "--cached", "--name-only").strip() == ("unassigned.md" if staged else "") + + +def test_empty_ownership_allows_only_metadata_commit(step_runtime): + runtime = step_runtime + runtime.responses = [{"status": "completed"}] + assert run_step(runtime) + assert runtime.git("log", "-1", "--format=%s").strip() == "chore(runtime): step 0 output" + assert runtime.git("diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD").strip() == "phases/runtime/index.json" + + +def test_owned_creation_modification_deletion_and_rename_commit_separately(step_runtime): + runtime = step_runtime + for name in ("modify.py", "delete.py", "old.py"): + (runtime.root / name).write_text("original content\n", encoding="utf-8") + runtime.git("add", "--", "modify.py", "delete.py", "old.py") + runtime.git("commit", "--quiet", "-m", "test: seed tracked files") + set_owned_paths(runtime, ["created.py", "modify.py", "delete.py", "old.py", "new.py"]) + + def change(runtime): + (runtime.root / "created.py").write_text("created\n", encoding="utf-8") + (runtime.root / "modify.py").write_text("modified\n", encoding="utf-8") + (runtime.root / "delete.py").unlink() + runtime.git("mv", "--", "old.py", "new.py") + return {"status": "completed"} + + runtime.responses = [change] + assert run_step(runtime) + assert set(runtime.git("diff-tree", "--no-commit-id", "--no-renames", "--name-only", "-r", "HEAD~1").splitlines()) == {"created.py", "modify.py", "delete.py", "old.py", "new.py"} + assert runtime.git("diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD").strip() == "phases/runtime/index.json" + assert runtime.git("status", "--porcelain").strip() == "" + + +@pytest.mark.parametrize("owned", [["old.py"], ["new.py"]]) +def test_rename_requires_both_endpoints(step_runtime, owned): + runtime = step_runtime + (runtime.root / "old.py").write_text("content\n", encoding="utf-8") + runtime.git("add", "--", "old.py") + runtime.git("commit", "--quiet", "-m", "test: seed rename") + set_owned_paths(runtime, owned) + + def rename(runtime): + runtime.git("mv", "--", "old.py", "new.py") + return {"status": "completed"} + + runtime.responses = [rename] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 1 + assert runtime.git("rev-list", "--count", "HEAD").strip() == "2" + assert (runtime.root / "new.py").is_file() + + +@pytest.mark.parametrize("injected_path", ["unassigned.md", "phases/runtime/index.json"]) +def test_new_unassigned_staged_path_is_rejected_before_commit(step_runtime, monkeypatch, injected_path): + runtime = step_runtime + set_owned_paths(runtime, ["approved.py"]) + + def change(runtime): + (runtime.root / "approved.py").write_text("approved\n", encoding="utf-8") + return {"status": "completed"} + + original_run_git = runtime.executor._run_git + + def inject_during_add(*args): + result = original_run_git(*args) + if "add" in args: + if injected_path == "unassigned.md": + (runtime.root / injected_path).write_text("late change\n", encoding="utf-8") + runtime.git("add", "--", injected_path) + return result + + monkeypatch.setattr(runtime.executor, "_run_git", inject_during_add) + runtime.responses = [change] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 1 + assert injected_path in runtime.executor._read_phase_index()["steps"][0]["error_message"] + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1" + assert (runtime.root / injected_path).is_file() + + +def test_owned_path_replaced_by_external_junction_is_rejected(step_runtime): + runtime = step_runtime + set_owned_paths(runtime, ["src/approved.py"]) + outside = runtime.root.parent / (runtime.root.name + "-outside") + outside.mkdir() + (outside / "approved.py").write_text("outside data\n", encoding="utf-8") + + def substitute(runtime): + link_directory(runtime, runtime.root / "src", outside) + return {"status": "completed"} + + runtime.responses = [substitute] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 1 + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1" + assert (outside / "approved.py").read_text(encoding="utf-8") == "outside data\n" + + +def test_late_unassigned_staging_survives_index_reset(step_runtime, monkeypatch): + runtime = step_runtime + set_owned_paths(runtime, ["approved.py"]) + + def change(runtime): + (runtime.root / "approved.py").write_text("approved\n", encoding="utf-8") + return {"status": "completed"} + + original_run_git = runtime.executor._run_git + + def inject_before_reset(*args): + if "reset" in args: + (runtime.root / "unassigned.md").write_text("staged user change\n", encoding="utf-8") + runtime.git("add", "--", "unassigned.md") + return original_run_git(*args) + + monkeypatch.setattr(runtime.executor, "_run_git", inject_before_reset) + runtime.responses = [change] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 1 + assert "unassigned.md" in runtime.git("diff", "--cached", "--name-only").splitlines() + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1" + + +@pytest.mark.parametrize("when", ["preflight", "after-attempt"]) +def test_owned_hard_link_is_rejected(step_runtime, when): + runtime = step_runtime + outside = runtime.root.parent / (runtime.root.name + "-outside.py") + outside.write_text("external data\n", encoding="utf-8") + set_owned_paths(runtime, ["approved.py"]) + + def substitute(runtime): + (runtime.root / "approved.py").hardlink_to(outside) + return {"status": "completed"} + + if when == "preflight": + substitute(runtime) + with pytest.raises(execute.HarnessError, match="owned_paths"): + runtime.executor._preflight_repository() + assert runtime.calls == [] + else: + runtime.responses = [substitute] + with pytest.raises(SystemExit) as stopped: + run_step(runtime) + assert stopped.value.code == 1 + assert outside.read_text(encoding="utf-8") == "external data\n" + assert runtime.git("rev-list", "--count", "HEAD").strip() == "1"