modify documents
This commit is contained in:
@@ -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": "<project>",
|
||||
"phase": "<phase>",
|
||||
"steps": [
|
||||
{"step": 0, "name": "<kebab-case-name>", "status": "pending"}
|
||||
{
|
||||
"step": 0, "name": "<kebab-case-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 <phase> --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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-<phase-name>` 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의 증거가 아니다.
|
||||
|
||||
+279
-308
@@ -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/<feature-id>/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/<model-id>/` 아래 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
|
||||
`<stem>_results.h5`와 `<stem>_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/<feature-id>/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)을 따른다.
|
||||
|
||||
+213
-456
@@ -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/<feature-id>/`의 승인된
|
||||
정식화·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/
|
||||
<case-dir>/ # 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-<phase-name>` 브랜치에서 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<T>` diagnostic으로 fail-closed 처리한다. `DofManager`,
|
||||
`SparseAssembler`와 `ResultRecovery`는 B33/MITC4 concrete storage가 아니라 위 runtime
|
||||
contract를 소비한다. Linear-static candidate는 runtime `Element`를
|
||||
`std::vector<std::unique_ptr<Element>>`로 소유하고, 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<ElementDefinition / ElementProperty / Material>
|
||||
├── 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 `<stem>_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/<step-name>/frames/0/nodal/displacement
|
||||
/steps/<step-name>/frames/0/nodal/reaction
|
||||
/steps/<step-name>/frames/0/element/end_force_local
|
||||
/steps/<step-name>/frames/0/element/section_resultant
|
||||
/steps/<step-name>/frames/0/element/generalized_strain
|
||||
/steps/<step-name>/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 `<stem>_results.h5`를 authoritative output으로 쓰며,
|
||||
같은 directory의 `<stem>_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를
|
||||
정리하고 불완전한 파일을 정상 `<stem>_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 <model.inp>
|
||||
```
|
||||
## 입력, 결과와 진단
|
||||
|
||||
CLI는 input과 같은 lexical sibling directory에 `<stem>_results.h5`와
|
||||
`<stem>_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를 선결정하지 않는다.
|
||||
|
||||
+1
-1
@@ -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보다 우선한다.
|
||||
|
||||
+61
-20
@@ -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 <phase-name>
|
||||
python scripts/execute.py <phase-name> --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 <phase-name>
|
||||
Remove-Item Env:FESA_HARNESS_CODEX_SANDBOX
|
||||
$env:FEM_HARNESS_CODEX_SANDBOX = "danger-full-access"
|
||||
python scripts/execute.py <phase-name> --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를 따른다.
|
||||
|
||||
+148
-69
@@ -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-id>/`의 승인된 선행 계약
|
||||
- 해당 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 <implementation-agent profile model>
|
||||
--config model_reasoning_effort="<profile effort>"
|
||||
--sandbox <workspace-write|danger-full-access>
|
||||
--dangerously-bypass-hook-trust
|
||||
--cd <repository-root>
|
||||
-
|
||||
```
|
||||
|
||||
기본값은 `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가 남아 있으면 실제 작업 지시와 충돌할 수 있다.
|
||||
|
||||
+129
-160
@@ -1,179 +1,148 @@
|
||||
# PRD: FESA 구조해석 솔버
|
||||
|
||||
## 목표
|
||||
FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 자체 유한요소 정식화로 구조해석을
|
||||
수행하고, step/frame 단위 결과를 input lexical sibling `<stem>_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/<feature-id>/`의 승인 계약으로 정의한다. 특정 기능의 코드가 존재한다는 사실과
|
||||
그 기능의 검증 완료 또는 현재 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/<model-id>/` 아래 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 `<stem>_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 <model.inp>` 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는 정확히 하나의 `<model.inp>`만 받고 `<stem>_results.h5`와 `<stem>_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-<FEATURE>-###` 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 `<stem>_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 `<stem>_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를 확인한다.
|
||||
|
||||
@@ -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/<model-id>/` 아래 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
|
||||
`<stem>_results.h5`와 `<stem>_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로 위장하지 않는다.
|
||||
@@ -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/
|
||||
<case-dir>/ # 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-<phase-name>` 브랜치에서 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<T>` diagnostic으로 fail-closed 처리한다. `DofManager`,
|
||||
`SparseAssembler`와 `ResultRecovery`는 B33/MITC4 concrete storage가 아니라 위 runtime
|
||||
contract를 소비한다. Linear-static candidate는 runtime `Element`를
|
||||
`std::vector<std::unique_ptr<Element>>`로 소유하고, 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<ElementDefinition / ElementProperty / Material>
|
||||
├── 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 `<stem>_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/<step-name>/frames/0/nodal/displacement
|
||||
/steps/<step-name>/frames/0/nodal/reaction
|
||||
/steps/<step-name>/frames/0/element/end_force_local
|
||||
/steps/<step-name>/frames/0/element/section_resultant
|
||||
/steps/<step-name>/frames/0/element/generalized_strain
|
||||
/steps/<step-name>/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 `<stem>_results.h5`를 authoritative output으로 쓰며,
|
||||
같은 directory의 `<stem>_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를
|
||||
정리하고 불완전한 파일을 정상 `<stem>_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 <model.inp>
|
||||
```
|
||||
|
||||
CLI는 input과 같은 lexical sibling directory에 `<stem>_results.h5`와
|
||||
`<stem>_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에서 제외한다.
|
||||
@@ -0,0 +1,179 @@
|
||||
# PRD: FESA 구조해석 솔버
|
||||
|
||||
## 목표
|
||||
FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 자체 유한요소 정식화로 구조해석을
|
||||
수행하고, step/frame 단위 결과를 input lexical sibling `<stem>_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/<model-id>/` 아래 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 `<stem>_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 <model.inp>` 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는 정확히 하나의 `<model.inp>`만 받고 `<stem>_results.h5`와 `<stem>_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 `<stem>_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 `<stem>_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 구현
|
||||
+221
-79
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다.
|
||||
Harness Step Executor — phase 내 Step을 순차 실행하고 결과와 파일 소유권을 검증한다.
|
||||
|
||||
Usage:
|
||||
python scripts/execute.py <phase-dir> [--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
|
||||
|
||||
|
||||
@@ -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/<feature-id>/([\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/<feature-id>/([\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<section>.*?)(?=\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/<feature-id>/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/<feature-id>/" 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 == []
|
||||
|
||||
+297
-9
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user