diff --git a/.claude/agents/diff-triager.md b/.claude/agents/diff-triager.md new file mode 100644 index 0000000..13df976 --- /dev/null +++ b/.claude/agents/diff-triager.md @@ -0,0 +1,33 @@ +--- +name: diff-triager +description: Triage golden-file regression failures for recordingtest. Classifies diffs between *.approved and *.received files into categories (real bug, missing normalization, environment drift, intentional change) and recommends next action. Use when a regression run fails or when the user asks "why did this test break?". +tools: Read, Grep, Glob, Bash +model: sonnet +--- + +You are **diff-triager**. Your job is forensic analysis of golden-file mismatches. + +## Input you should seek + +- `baselines/.approved.*` and the corresponding `*.received.*` +- The scenario file under `scenarios/` +- Failure artifacts: UIA tree dump, engine sidecar JSON, input log, screenshot +- Recent git log on SUT binary path and `normalizer/` rules + +## Classification buckets + +1. **Real regression** — SUT behavior changed unintentionally. Recommend: file bug, keep baseline. +2. **Intentional change** — feature work changed output. Recommend: `/approve` after human confirmation. +3. **Normalization gap** — diff is noise (timestamp, GUID, float tolerance, ordering). Recommend: add rule to normalizer. +4. **Environment drift** — DPI, locale, GPU, plugin load order. Recommend: fix env or quarantine. +5. **Flaky / timing** — non-deterministic; recommend retry + root-cause in player sync. + +## Output + +Short report per failure: +- Bucket +- Evidence (specific diff lines) +- Recommended action (one of: file bug / approve / add normalizer rule / fix env / investigate flake) +- Confidence (low/medium/high) + +Do not mutate baselines or scenarios yourself. Only recommend. diff --git a/.claude/agents/evaluator.md b/.claude/agents/evaluator.md new file mode 100644 index 0000000..91c91c0 --- /dev/null +++ b/.claude/agents/evaluator.md @@ -0,0 +1,45 @@ +--- +name: evaluator +description: Grade a completed module or feature against its Sprint Contract. Independent from the Generator — reads the contract, exercises the artifact, scores each Definition-of-Done item, and reports pass/fail with evidence. Use after the Generator reports "done" but before the work is merged or marked complete in PROGRESS.md. +tools: Read, Grep, Glob, Bash +model: sonnet +--- + +You are **evaluator**. You are deliberately *not* the agent that built the thing. Your value comes from independent verification. + +## Inputs +- `docs/contracts/.md` — the Sprint Contract +- The generator's artifact (code, scenario, baseline, catalog…) +- Any fixtures or oracles named in the contract + +## Method +1. Read the contract. If missing, refuse and tell the caller to run `planner` first. +2. For each DoD item: + - Execute the stated verification (script, diff, inspection). + - Record **evidence** (command output, file path, diff snippet). + - Score: `pass` / `fail` / `partial` / `untestable`. +3. Compute an overall verdict: pass only if all items pass. +4. Write a report to `docs/contracts/.evaluation.md` with timestamp. +5. If any fail, **do not** mark PROGRESS.md as done. Return the report to the caller. + +## Rules +- No self-praise, no charity. Treat ambiguous results as `partial` or `untestable`. +- Never modify the artifact you are grading. You may only run read/execute commands. +- If a DoD item cannot be tested with the available tools, flag it `untestable` and explain — do not fake a pass. +- Keep the report terse: one bullet per DoD item with evidence link. + +## Output format + +```markdown +# Evaluation — () + +Verdict: **pass** | **fail** + +| # | DoD item | Score | Evidence | +|---|----------|-------|----------| +| 1 | ... | pass | logs/eval-1.txt | +| 2 | ... | fail | diff snippet | + +## Notes + +``` diff --git a/.claude/agents/planner.md b/.claude/agents/planner.md new file mode 100644 index 0000000..e3424c8 --- /dev/null +++ b/.claude/agents/planner.md @@ -0,0 +1,55 @@ +--- +name: planner +description: Convert a natural-language request or module goal into a concrete PLAN.md entry plus a Sprint Contract that defines "done". Use at the start of any non-trivial module or feature work, before generator-style implementation begins. +tools: Read, Write, Edit, Glob, Grep +model: sonnet +--- + +You are **planner**. You translate vague asks into *contracts* that a separate Generator agent can implement against and a separate Evaluator agent can grade. + +## Inputs +- User request (may be a sentence) +- Current `PLAN.md`, `PROGRESS.md`, `CLAUDE.md` +- Relevant memory under `~/.claude/projects/.../memory/` + +## Outputs +1. A new entry (or update) in `PLAN.md` with priority and dependencies. +2. A **Sprint Contract** file at `docs/contracts/.md` using the template below. +3. A short briefing back to the caller (≤10 lines) summarizing what was written. + +## Sprint Contract template + +```markdown +# Sprint Contract — + +**Owner:** +**Depends on:** +**Issue:** # + +## Goal + + +## Definition of Done (grading criteria) +- [ ] +- [ ] +- [ ] + +## Interfaces / contracts +- Inputs: +- Outputs: +- Side effects: + +## Out of scope +- + +## Evaluation plan +How the evaluator agent will verify each DoD item (commands, fixtures, oracles). + +## Risks / open questions +``` + +## Rules +- Never implement. Never write code into `src/`. Only plan documents. +- DoD items must be **objectively checkable** — no "works well", "is clean". +- If the request is ambiguous, write the contract with explicit `TODO(user):` lines and stop. +- Keep criteria ≤7. More than that means the scope should be split. diff --git a/.claude/agents/scenario-author.md b/.claude/agents/scenario-author.md new file mode 100644 index 0000000..6a3b2e0 --- /dev/null +++ b/.claude/agents/scenario-author.md @@ -0,0 +1,39 @@ +--- +name: scenario-author +description: Translate a natural-language manual-test description into a structured recordingtest scenario file (JSON/YAML) with element-aware steps, checkpoints, and expected baseline artifacts. Use when the user wants to add a new regression scenario without recording it live. +tools: Read, Write, Glob, Grep +model: sonnet +--- + +You are **scenario-author**. You convert prose into scenario files under `scenarios/`. + +## Scenario schema (draft) + +```yaml +name: +description: +sut: + exe: "EG-BIM Modeler/EG-BIM Modeler.exe" + startup_timeout_ms: 15000 +steps: + - kind: click | type | drag | hotkey | wait | checkpoint | save + target: + uia_path: "MainWindow/Toolbar/Button[@Name='Box']" # when available + offset: [x, y] # fallback for 3D viewport + value: + wait_for: +checkpoints: + - after_step: 5 + save_as: scenarios//checkpoint-1 +baselines: + - path: baselines/.approved.hme + normalize_with: [default, floats_e6, strip_timestamps] +``` + +## Rules + +- Prefer UIA element paths over raw coordinates. Only use `offset` for 3D viewport interaction. +- Always insert at least one checkpoint + final save baseline. +- Pick normalization profiles from existing rules; if unsure, add a TODO and ask the user. +- Never invent UIA paths you have not verified via sut-explorer output. Mark unknowns with `TODO:`. +- Write the scenario file and return a terse summary with the file path. diff --git a/.claude/agents/sut-explorer.md b/.claude/agents/sut-explorer.md new file mode 100644 index 0000000..0dbe32b --- /dev/null +++ b/.claude/agents/sut-explorer.md @@ -0,0 +1,30 @@ +--- +name: sut-explorer +description: Analyze the EG-BIM Modeler SUT folder — enumerate MEF plugins, dump Json/ config files, inspect HmEG engine assemblies, and produce a catalog for the recordingtest automation tool. Use when building or refreshing sut-prober outputs, or when the user asks about SUT structure, plugins, or settings. +tools: Read, Glob, Grep, Bash, Write +model: sonnet +--- + +You are **sut-explorer**, a read-only analyst for the SUT (System Under Test) living at `EG-BIM Modeler/` in the recordingtest repo. + +## Responsibilities + +1. Enumerate MEF plugins under `EG-BIM Modeler/Plugins/Eg*Plugin/` and produce a catalog (plugin name, main dll, any manifest). +2. Snapshot `EG-BIM Modeler/Json/*.json` contents and identify non-deterministic fields (timestamps, GUIDs, absolute paths, recent file lists). +3. Inspect HmEG/HmGeometry/Editor*.dll assemblies (names, versions) — use `Bash` with `dotnet` or `strings` if available, but **never execute the SUT**. +4. Write results to `docs/sut-catalog/` as markdown + JSON. + +## Rules + +- **Never launch `EG-BIM Modeler.exe`**. Static analysis only. +- **Never modify** the `EG-BIM Modeler/` folder. +- Keep outputs diff-friendly: sorted, stable ordering, no absolute paths. +- If asked to do something outside this scope, decline and suggest the right agent/command. + +## Output format + +Return a short summary to the caller and write detailed catalogs to `docs/sut-catalog/`. Always list: +- Plugin count and notable categories +- Json config files and suspected non-deterministic fields +- Engine assembly list with versions (if derivable) +- Follow-up questions for the user diff --git a/.claude/commands/approve.md b/.claude/commands/approve.md new file mode 100644 index 0000000..0b4d80d --- /dev/null +++ b/.claude/commands/approve.md @@ -0,0 +1,21 @@ +--- +name: approve +description: Promote a received baseline to approved after human confirmation. Usage /approve +allowed-tools: Read, Bash, Write, Glob +--- + +Promote a received golden file to an approved baseline. + +Scenario name: `$ARGUMENTS` + +Steps: + +1. Refuse if `$ARGUMENTS` is empty. Ask the user for a scenario name. +2. Locate `baselines/$ARGUMENTS.received.*` files. Refuse if none exist. +3. Show the diff against the existing `*.approved.*` (if any) so the user can confirm the intent. +4. **Ask the user to type an approval reason** (intentional change / bug fix / new feature etc.). Record it. +5. Copy `*.received.*` → `*.approved.*`. +6. Write an entry to `docs/history/YYYY-MM-DD_approve-$ARGUMENTS.md` with the reason, diff summary, context usage, and duration. +7. Update `PROGRESS.md` baseline log. + +Never approve without an explicit reason from the user. diff --git a/.claude/commands/contract.md b/.claude/commands/contract.md new file mode 100644 index 0000000..9a8ed63 --- /dev/null +++ b/.claude/commands/contract.md @@ -0,0 +1,16 @@ +--- +name: contract +description: Create or update a Sprint Contract for a module/feature via the planner agent. Usage /contract +allowed-tools: Read, Write, Edit, Agent, Glob, Grep +--- + +Create a Sprint Contract and PLAN.md entry for: `$ARGUMENTS` + +Delegate to the **planner** subagent. The planner must: + +1. Read `PLAN.md`, `PROGRESS.md`, `CLAUDE.md`, and relevant memory. +2. Write `docs/contracts/.md` with the Sprint Contract template (Goal, DoD, Interfaces, Out of scope, Evaluation plan, Risks). +3. Add or update the corresponding `PLAN.md` entry with priority and dependencies. +4. Return a short briefing with file paths. + +If `$ARGUMENTS` is empty, ask the user what to plan. Never write code into `src/` — planning only. diff --git a/.claude/commands/evaluate.md b/.claude/commands/evaluate.md new file mode 100644 index 0000000..477ba18 --- /dev/null +++ b/.claude/commands/evaluate.md @@ -0,0 +1,20 @@ +--- +name: evaluate +description: Grade a completed module against its Sprint Contract via the evaluator agent. Usage /evaluate +allowed-tools: Read, Glob, Grep, Bash, Agent +--- + +Evaluate module: `$ARGUMENTS` + +Delegate to the **evaluator** subagent. It must: + +1. Read `docs/contracts/$ARGUMENTS.md`. Refuse if missing. +2. For each Definition-of-Done item, run the verification named in the contract's Evaluation plan. +3. Collect evidence (command output, diffs, file paths). +4. Write `docs/contracts/$ARGUMENTS.evaluation.md` with the verdict table. +5. Return the verdict to the caller. + +If verdict is **fail**, do NOT mark PROGRESS.md as done — report back so the generator can iterate. +If verdict is **pass**, the caller (not the evaluator) may update PROGRESS.md. + +Never let the generator and evaluator be the same agent in a single session. diff --git a/.claude/commands/handoff.md b/.claude/commands/handoff.md new file mode 100644 index 0000000..002fecc --- /dev/null +++ b/.claude/commands/handoff.md @@ -0,0 +1,23 @@ +--- +name: handoff +description: Session handoff — update PROGRESS.md, PLAN.md, and write the history file in one go before ending a work session. +allowed-tools: Read, Write, Edit, Bash +--- + +Prepare a clean handoff so the next agent can pick up. + +Steps: + +1. Read `PROGRESS.md` and `PLAN.md`. If either is missing, create a skeleton. +2. Ask the user (or infer from the session) what was completed, what is in progress, and what is blocked. +3. Update `PROGRESS.md`: + - Move completed items to "Done" with date and artifact paths. + - Mark in-progress items with current owner and status. +4. Update `PLAN.md`: + - Remove completed items. + - Add any newly discovered follow-ups. + - Re-order priorities if needed. +5. Write `docs/history/YYYY-MM-DD_{작업명}.md` with **소요 시간**, **Context 사용량**, 이슈 번호, 산출물. +6. Confirm all three files are saved and list their paths. + +Arguments: `$ARGUMENTS` — optional short description to seed the history file title. diff --git a/.claude/commands/progress.md b/.claude/commands/progress.md new file mode 100644 index 0000000..ddb6bcc --- /dev/null +++ b/.claude/commands/progress.md @@ -0,0 +1,16 @@ +--- +name: progress +description: Print a concise summary of PROGRESS.md and PLAN.md so the agent can decide what to work on next. +allowed-tools: Read +--- + +Read `PROGRESS.md` and `PLAN.md` and produce a short status report: + +- **Done recently** (last 5 items) +- **In progress** (with owner) +- **Blocked** +- **Next up** (top 3 from PLAN.md) + +If either file is missing, say so and suggest running `/handoff` to bootstrap them. + +Keep the report under 20 lines. diff --git a/.claude/commands/regress.md b/.claude/commands/regress.md new file mode 100644 index 0000000..4a21a87 --- /dev/null +++ b/.claude/commands/regress.md @@ -0,0 +1,19 @@ +--- +name: regress +description: Run the full recordingtest regression suite (or a filtered subset) and triage failures. +allowed-tools: Bash, Read, Glob, Grep, Agent, Write +--- + +Run the regression suite. + +Steps: + +1. Verify the runner exists. If `src/Recordingtest.Runner/` is not yet built, stop and tell the user the suite is not set up. +2. Execute the runner with optional filter `$ARGUMENTS` (empty = all scenarios). +3. Collect results from the runner output folder. +4. For each failed scenario, delegate to the `diff-triager` subagent with the baseline/received/artifact paths. +5. Summarize: passed / failed / triage buckets. +6. If any failures are classified as "normalization gap", list suggested rules at the end. +7. Append run summary to `PROGRESS.md` under a "Recent regression runs" section. + +Do NOT auto-approve or mutate baselines. Human confirmation is required via `/approve`. diff --git a/.claude/commands/sut-probe.md b/.claude/commands/sut-probe.md new file mode 100644 index 0000000..e5bb434 --- /dev/null +++ b/.claude/commands/sut-probe.md @@ -0,0 +1,19 @@ +--- +name: sut-probe +description: Static probe of the EG-BIM Modeler SUT — enumerate plugins, snapshot Json/ configs, list engine assemblies. Does NOT launch the SUT. +allowed-tools: Read, Glob, Grep, Bash, Write, Agent +--- + +Run a static analysis pass on the SUT at `EG-BIM Modeler/` and produce a catalog. + +Delegate to the `sut-explorer` subagent with this scope: + +1. List every plugin folder under `EG-BIM Modeler/Plugins/` and count them. +2. Read each `EG-BIM Modeler/Json/*.json` and flag non-deterministic fields. +3. List core assemblies (`HmEG*.dll`, `Editor*.dll`, `HmGeometry*.dll`) with file sizes. +4. Write the catalog to `docs/sut-catalog/catalog.md` and `docs/sut-catalog/plugins.json`. +5. Report a concise summary back here. + +Arguments (optional): $ARGUMENTS — if provided, restrict analysis to that subpath (e.g. `Plugins/EgBoxPlugin`). + +After the subagent reports, update `PROGRESS.md` with the catalog timestamp. diff --git a/.claude/hooks/guard-sut-folder.sh b/.claude/hooks/guard-sut-folder.sh new file mode 100644 index 0000000..5759e6a --- /dev/null +++ b/.claude/hooks/guard-sut-folder.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# PreToolUse(Edit|Write) hook: block modifications to the EG-BIM Modeler/ folder. +# The SUT binary tree is read-only from recordingtest's perspective. +set -e + +input=$(cat) +path=$(echo "$input" | jq -r '.tool_input.file_path // ""') + +case "$path" in + */EG-BIM\ Modeler/*|*"EG-BIM Modeler"*) + >&2 echo "🚫 EG-BIM Modeler/ is the SUT binary tree and must not be modified by recordingtest. Use docs/sut-catalog/ for derived data." + exit 2 + ;; +esac +exit 0 diff --git a/.claude/hooks/guard-sut-launch.sh b/.claude/hooks/guard-sut-launch.sh new file mode 100644 index 0000000..5ac2c31 --- /dev/null +++ b/.claude/hooks/guard-sut-launch.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# PreToolUse(Bash) hook: warn if the agent is about to launch the SUT binary +# without going through the runner/player. Does not block; just informs. +set -e + +input=$(cat) +cmd=$(echo "$input" | jq -r '.tool_input.command // ""') + +if echo "$cmd" | grep -qi 'EG-BIM Modeler\.exe'; then + jq -n '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + additionalContext: "⚠ SUT 실행 감지: EG-BIM Modeler.exe 는 player/runner 를 통해서만 실행하세요. sut-explorer 는 정적 분석만 허용됩니다." + } + }' +fi diff --git a/.claude/hooks/session-start-progress.sh b/.claude/hooks/session-start-progress.sh new file mode 100644 index 0000000..87d25a9 --- /dev/null +++ b/.claude/hooks/session-start-progress.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# SessionStart hook: surface PROGRESS.md and PLAN.md so any agent can pick up work. +set -e + +ctx="" +for f in PROGRESS.md PLAN.md; do + if [ -f "$f" ]; then + ctx="${ctx} + +=== $f === +$(head -80 "$f")" + else + ctx="${ctx} + +=== $f === +(missing — run /handoff to bootstrap)" + fi +done + +# Emit JSON so Claude Code adds it as additionalContext. +jq -n --arg c "$ctx" '{ + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: $c + } +}' diff --git a/.claude/hooks/stop-handoff-reminder.sh b/.claude/hooks/stop-handoff-reminder.sh new file mode 100644 index 0000000..074e5b7 --- /dev/null +++ b/.claude/hooks/stop-handoff-reminder.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Stop hook: remind the agent to run /handoff if PROGRESS.md / PLAN.md / today's +# history file look stale. Informational only — never blocks. +set -e + +today=$(date +%Y-%m-%d) +msg="" + +if [ ! -f PROGRESS.md ]; then msg="${msg}\n- PROGRESS.md missing"; fi +if [ ! -f PLAN.md ]; then msg="${msg}\n- PLAN.md missing"; fi +if ! ls "docs/history/${today}_"*.md >/dev/null 2>&1; then + msg="${msg}\n- 오늘 날짜의 history 파일이 없습니다 (${today})" +fi + +if [ -n "$msg" ]; then + jq -n --arg m "작업 종료 전 확인:${msg}\n→ /handoff 실행 권장" '{ + hookSpecificOutput: { + hookEventName: "Stop", + additionalContext: $m + } + }' +fi diff --git a/.claude/settings.json b/.claude/settings.json index 727a9e3..08ba909 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -3,10 +3,55 @@ "allow": [ "mcp__gitea__get_me", "mcp__gitea__create_repo", - "mcp__gitea__issue_write" + "mcp__gitea__issue_write", + "mcp__gitea__issue_read", + "Edit(/.claude/skills/golden-file-normalizer/**)", + "Edit(/.claude/skills/flaui-cookbook/**)" ], "additionalDirectories": [ "C:\\Users\\nbright\\.claude" ] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/session-start-progress.sh" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/guard-sut-launch.sh" + } + ] + }, + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/guard-sut-folder.sh" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/stop-handoff-reminder.sh" + } + ] + } + ] } } diff --git a/.claude/skills/flaui-cookbook/SKILL.md b/.claude/skills/flaui-cookbook/SKILL.md new file mode 100644 index 0000000..72a46db --- /dev/null +++ b/.claude/skills/flaui-cookbook/SKILL.md @@ -0,0 +1,53 @@ +--- +name: flaui-cookbook +description: FlaUI and UI Automation recipes for the recordingtest project — waiting strategies, element finding patterns, pattern invocation, and integration with element-aware recording. Use when writing recorder/player code or diagnosing flaky UIA interactions. +--- + +# FlaUI cookbook + +## Dependencies +- `FlaUI.Core`, `FlaUI.UIA3` (prefer UIA3 over UIA2) +- Target: `net8.0-windows` or higher + +## Launching the SUT + +```csharp +var app = FlaUI.Core.Application.Launch("EG-BIM Modeler/EG-BIM Modeler.exe"); +using var automation = new UIA3Automation(); +var main = app.GetMainWindow(automation, TimeSpan.FromSeconds(30)); +``` + +## Finding elements (prefer AutomationId > Name > ClassName) + +```csharp +var btn = main.FindFirstDescendant(cf => cf.ByAutomationId("BoxCommand")).AsButton(); +``` + +## Waiting — NEVER use fixed sleep + +```csharp +Retry.WhileNull( + () => main.FindFirstDescendant(cf => cf.ByName("Ready")), + timeout: TimeSpan.FromSeconds(10), + interval: TimeSpan.FromMilliseconds(100)); +``` + +For plugin load completion, wait on a known UIA element from a late-loading plugin, not a timer. + +## Element path capture (for element-aware recording) + +Walk ancestors and emit `ClassName[@AutomationId='…']/ClassName[@Name='…']` — resilient to layout changes. + +## 3D viewport fallback + +SharpDX D3D11 surface is a UIA dead zone. Record: +1. The hosting element's UIA path +2. A normalized offset `(dx/width, dy/height)` inside that element +3. The engine state sidecar AFTER the interaction + +## Common pitfalls + +- Calling UIA from the wrong thread — always marshal to STA. +- Stale cached elements after modal dialogs — re-find after focus change. +- IME composition swallows keys — use clipboard paste for Korean/Japanese input. +- MahApps Flyouts are not descendants of MainWindow; search from the desktop root. diff --git a/.claude/skills/golden-file-normalizer/SKILL.md b/.claude/skills/golden-file-normalizer/SKILL.md new file mode 100644 index 0000000..f63c796 --- /dev/null +++ b/.claude/skills/golden-file-normalizer/SKILL.md @@ -0,0 +1,37 @@ +--- +name: golden-file-normalizer +description: Guidance and recipes for writing normalization rules that make SUT output files deterministic for golden-file regression testing. Use when designing or extending the normalizer module, or when diagnosing diff noise. +--- + +# Golden-file normalizer skill + +When writing or reviewing normalization rules for recordingtest, apply this checklist. + +## Canonical sources of non-determinism + +| Category | Example patterns | Rule strategy | +|----------|------------------|---------------| +| Timestamps | ISO8601, Unix epoch, `"saved": "2026-..."` | Replace with `` or strip key | +| GUIDs / UUIDs | `xxxxxxxx-xxxx-...` | Replace with `` (stable index per occurrence) | +| Absolute paths | `C:\Users\...\`, `D:\MYCLAUDE_PROJECT\...` | Replace repo root with ``, user with `` | +| Recent files | `RecentFiles.json` | Empty the list or mask entirely | +| Float precision | `3.14159265358979` | Round to configured epsilon (default 1e-6) | +| Collection ordering | unsorted dict/list | Sort by canonical key | +| Machine name / locale | `DESKTOP-XXXX`, `ko-KR` | Mask or pin | +| GPU/driver hashes | inside render metadata | Strip | + +## Rule authoring principles + +1. **Rules are versioned** — bump the normalizer profile when adding/removing rules; scenarios pin a profile. +2. **Never hide real bugs** — mask only fields proven non-deterministic across 3+ clean runs. +3. **Text first** — parse JSON/XML semantically; use regex only as fallback. +4. **Bidirectional tests** — every rule has a unit test with before/after samples. +5. **Log what you normalized** — emit a sidecar `normalization.log` listing replacements for diagnostics. + +## Output + +When the user asks for a new rule, produce: +- Rule name and profile membership +- Regex or parser snippet (C#) +- Unit test sample input/output +- A note on which SUT file(s) it applies to diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..607ae48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# SUT 바이너리 트리 — recordingtest 저장소에는 포함하지 않음. +# 로컬 테스트용으로만 동봉하며, 각 개발자가 별도로 배치한다. +EG-BIM Modeler/ + +# Build outputs +bin/ +obj/ +*.user +*.suo +.vs/ + +# Logs & temp +*.log +hmlogs/ +Log/ + +# Received (not yet approved) golden files — baselines만 커밋 +baselines/**/*.received.* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8095113 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,169 @@ +# CLAUDE.md — recordingtest + +이 파일은 Claude Code가 본 저장소에서 작업할 때 항상 읽는 프로젝트 운영 지침이다. + +## 0. 세션 시작 시 필독 (모든 에이전트) + +여러 에이전트가 작업을 분담하므로, **세션을 시작하는 모든 에이전트는 가장 먼저 다음 두 파일을 읽는다:** + +1. [PROGRESS.md](PROGRESS.md) — 지금까지 *무엇이 끝났는가*. 모듈별 진행 상태, 최근 완료 작업, 현재 차단 이슈. +2. [PLAN.md](PLAN.md) — 앞으로 *무엇을 해야 하는가*. 모듈별 To-Do, 담당 에이전트, 우선순위, 의존 관계. + +읽고 나서 자신이 맡을 작업을 PLAN.md에서 고르고, 시작 시 PROGRESS.md에 "in progress"로 표시한다. 작업이 끝나면: +- PROGRESS.md 의 해당 항목을 "done"으로 옮기고 날짜·결과·산출물 경로 기록 +- PLAN.md 의 완료 항목 제거 또는 다음 단계로 갱신 +- `docs/history/YYYY-MM-DD_{작업명}.md` 히스토리 파일 작성 (소요 시간·Context 사용량 필수) + +**원칙:** PROGRESS.md와 PLAN.md는 *에이전트 간 공유 메모리*다. 자기 머릿속에만 두지 말고 반드시 파일에 반영해야 다음 에이전트가 이어받을 수 있다. 충돌 시 최신 커밋 기준으로 머지하고, 모호하면 사용자에게 질문한다. + +## 0.1 작업 사이클 — Planner → Generator → Evaluator + +Anthropic의 "Harness Design for Long-Running Agent Applications" 설계 원칙을 채택한다. +핵심: **생성자와 평가자를 같은 에이전트가 겸하지 않는다**. 자기 작업을 과대평가하는 편향을 피하기 위해서다. + +모든 비자명한(non-trivial) 모듈/기능 작업은 다음 3단계를 거친다: + +1. **Planner (`/contract `)** — 사용자의 요청을 Sprint Contract로 변환. + - 산출물: `docs/contracts/.md` (Goal, **Definition of Done**, Interfaces, Out of scope, Evaluation plan, Risks) + - DoD 항목은 **객관적으로 검증 가능**해야 한다. "잘 동작한다"는 금지. + - `PLAN.md`에 해당 항목 추가. + +2. **Generator** — Sprint Contract를 계약으로 삼고 실제 구현. 일반 세션 또는 전용 구현 에이전트가 수행. + - 계약을 읽고 DoD 항목만 충족시키는 데 집중. + - 스코프 이탈 금지. 범위 변경이 필요하면 planner를 다시 호출. + +3. **Evaluator (`/evaluate `)** — 독립된 `evaluator` 서브에이전트가 계약 기준으로 채점. + - 산출물: `docs/contracts/.evaluation.md` (verdict + evidence table) + - **fail**이면 PROGRESS.md에 done으로 옮기지 않는다. Generator가 재작업. + - **pass**여야만 호출자가 PROGRESS.md를 갱신한다. + +### 컨텍스트 위생 (context hygiene) + +- 긴 작업 중 컨텍스트가 차면 요약(compaction)하지 말고 **파일에 상태를 쏟고 새 세션으로 리셋**한다. PROGRESS.md/PLAN.md/Sprint Contract가 그 인계 창구다. +- Stop hook이 핸드오프 누락을 경고한다. 무시하지 말 것. +- 모델/하네스가 진화하므로 `.claude/` 비계는 주기적으로 감사·축소한다 (PLAN.md에 "scaffolding review" 상시 항목 유지). + +## 1. 프로젝트 정체성 + +**recordingtest** 는 사내 WPF 3D 편집 응용(자체 개발, Rhino3D 유사)에 대한 **사용자 입력 회귀 테스트 자동화 도구**다. 도구 자체이지 SUT가 아니다. + +### SUT(System Under Test) 개요 +- WPF 데스크톱 애플리케이션, Main Window 1개 Rhino3D 유사 UI +- 자체 3D 엔진 **HmEG** (Helix toolkit 유사) +- **MEF 기반 plug-in 아키텍처** — 기능별로 독립 C# 프로젝트 +- 결과물: 자체 포맷의 모델(.hmeg)/프로젝트 저장 파일 + +## 2. 핵심 전략 — Golden-file 회귀 + +> 수동 테스트 입력을 레코딩 → 리플레이 → 결과 저장 파일을 베이스라인과 diff. + +ApprovalTests 패턴과 동형. SUT 코드 변경 협조를 최소화하기 위한 의도적 선택. + +``` +[수동 테스트] → 입력 레코드 + 결과 파일 A (baseline) +[회귀 시점] → 입력 리플레이 → 결과 파일 B → normalize → diff(A, B) +``` + +**우선순위:** +- 1순위: recorder, player, 정규화(normalizer), diff-reporter +- 2순위: engine-bridge (엔진 상태 sidecar JSON 덤프) +- 후순위: viewport-verifier (픽셀/이미지 비교) — golden-file로 못 잡는 케이스 보강용 + +## 3. 아키텍처 구성요소(예정) + +| 모듈 | 책임 | +|------|------| +| `sut-prober` | 대상 앱 실행, UIA 트리·MEF plugin 목록 덤프 | +| `recorder` | 입력 캡처(키/마우스/포커스) + element path + offset 동시 저장 | +| `player` | 시나리오 재생, 비동기 작업 동기화 | +| `normalizer` | 저장 파일 정규화(타임스탬프/GUID/경로/부동소수점/순서) | +| `diff-reporter` | baseline vs received diff, 갈라진 지점 시각화 | +| `engine-bridge` | HmEG 내부 상태(카메라/선택/씬그래프) sidecar JSON 노출 | +| `test-runner` | 시나리오 묶음 실행, 리포트 | +| `viewport-verifier` | (후순위) 3D 스크린샷 SSIM 비교 | + +각 모듈은 독립 PoC → 통합 순서로 진행한다. + +## 4. 기술 스택 가이드 + +- **언어**: C# / .NET (SUT와 동일 생태계, in-process probe 가능) +- **UI 자동화**: **FlaUI** 1순위 (UIA 기반, .NET 네이티브). WinAppDriver/Appium은 fallback. +- **저수준 입력**: Win32 SetWindowsHookEx (low-level mouse/keyboard) — element 매칭과 hybrid +- **시나리오 포맷**: JSON 또는 YAML (git diff 친화적). 바이너리 금지. +- **베이스라인 파일**: `*.approved.{ext}` / `*.received.{ext}` 명명 규칙 +- **이미지/큰 베이스라인**: Git LFS + +## 5. 반드시 지킬 설계 원칙 + +1. **결정성(Determinism) 우선** — 비결정적 요소(시각·랜덤·경로·GUID·부동소수점·컬렉션 순서)는 정규화 파이프라인을 통과해야 한다. 새 필드 추가 시 정규화 규칙 동시 등록. +2. **Element-aware 입력 캡처** — 좌표만 저장 금지. 항상 UIA element path + 상대 offset을 같이 기록해 해상도/DPI/창크기 변화에 견디게 한다. +3. **타이밍 동기화** — 고정 sleep 금지. UIA 이벤트, property 변경, plugin 로드 완료 신호를 대기한다. +4. **Dispatcher marshaling** — SUT 내부 probe/hook 코드는 반드시 WPF UI thread 위에서 동작. +5. **체크포인트** — 한 시나리오 안에서 여러 번 저장→비교 가능해야 한다(이분 탐색용). +6. **실패 아티팩트 풀세트** — 실패 시 스크린샷, UIA 트리 덤프, 엔진 상태 sidecar, 입력 로그, diff를 한 폴더에 동시 저장. +7. **SUT 침습 최소화** — AutomationPeer/probe 부착이 필요하면 별도 어셈블리로 격리하고 SUT 팀과 합의 후 진행. +8. **민감정보 마스킹** — 레코딩에 비밀번호/토큰 포함 금지. + +## 6. 환경 제약 + +- **세션 0 불가**: WPF는 대화형 데스크톱 세션 필요 → CI에서 헤드리스 불가, RDP/대화형 agent 필요 +- **DPI/멀티모니터 정규화**: 테스트 머신은 고정 DPI 권장 +- **GPU 의존**: 3D 렌더 결과는 드라이버 영향 → 픽셀 비교는 항상 톨러런스 + 마스킹 + +## 7. 작업 흐름 규칙 + +### 히스토리 기록 (필수) +모든 작업 완료 시 `docs/history/YYYY-MM-DD_{작업명}.md` 작성. 필수 항목: +- **소요 시간** +- **Context 사용량** +- 관련 이슈 (#N) + +누락 시 저장이 차단된다. + +### 저장소 +- Origin: https://gitea.hmac.kr/kimminsung/recordingtest +- 이슈 트래커: 동일 Gitea +- PR/커밋 메시지에 이슈 번호(#N) 참조 + +### Claude 작업 원칙 +- **세션 시작 시 PROGRESS.md / PLAN.md 먼저 읽기** (§0 참조) +- 코드 변경 전 관련 파일 read 필수 +- 테스트 자동화 도구 자체의 회귀를 위해 본 저장소 코드도 단위 테스트 보유 권장 +- 의존성 추가는 사전에 사용자 확인 +- 메모리 시스템(`~/.claude/projects/.../memory/`)에 프로젝트 진행 상태/전략 결정 보존 +- 작업 종료 시 PROGRESS.md / PLAN.md 업데이트 + 히스토리 파일 작성 (3종 세트) + +## 8. 디렉터리 구조 (예정 — 셋업 시 확정) + +``` +recordingtest/ +├── src/ +│ ├── Recordingtest.Recorder/ +│ ├── Recordingtest.Player/ +│ ├── Recordingtest.Normalizer/ +│ ├── Recordingtest.DiffReporter/ +│ ├── Recordingtest.EngineBridge/ +│ ├── Recordingtest.SutProber/ +│ └── Recordingtest.Runner/ +├── tests/ +├── scenarios/ # 시나리오 JSON/YAML +├── baselines/ # *.approved.* (LFS 후보) +├── docs/ +│ ├── history/ # 작업 히스토리 +│ ├── contracts/ # Sprint Contracts + evaluations +│ └── sut-catalog/ # sut-explorer 산출물 +├── PROGRESS.md +├── PLAN.md +└── CLAUDE.md +``` + +## 9. 비목표 (Out of Scope) + +- SUT 자체의 기능 변경/버그 수정 +- 일반 웹/모바일 자동화 +- 부하·성능 테스트 +- 단위 테스트 프레임워크 대체 + +## 10. 결정 로그 위치 + +주요 기술 결정과 그 근거는 `docs/history/`와 Claude 메모리(`project_recordingtest_*`)에 분산 저장된다. 새 결정 시 반드시 둘 다 갱신한다. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..aca73f5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,10 @@ + + + net8.0 + latest + enable + enable + true + Recordingtest.$(MSBuildProjectName.Replace('Recordingtest.','')) + + diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..c9c9357 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,45 @@ +# PLAN.md — recordingtest + +> 다음에 할 일의 *우선순위 큐*. 에이전트는 상단부터 집어든다. +> 비자명한 항목은 `/contract ` → `docs/contracts/.md` 로 Sprint Contract 먼저 작성. + +## P0 — 지금 바로 + +1. **sut-prober PoC 구현** — Sprint Contract: [docs/contracts/sut-prober.md](docs/contracts/sut-prober.md) + - Generator: 일반 세션 + - Evaluator: `/evaluate sut-prober` + - 의존: 없음 (독립 실행) +2. **PROGRESS.md / PLAN.md / CLAUDE.md 훅 동작 검증** — SessionStart/Stop/Guard 3개 shell 스크립트를 실제로 트리거시켜 확인 + - 의존: jq 설치 여부 확인 + +## P1 — PoC 1단계 + +3. **normalizer PoC** — Sprint Contract: [docs/contracts/normalizer.md](docs/contracts/normalizer.md) + - 의존: sut-prober의 Json 카탈로그 +4. **recorder PoC (element-aware)** — Sprint Contract: [docs/contracts/recorder.md](docs/contracts/recorder.md) + - 의존: FlaUI 패키지 승인 +5. **player PoC** — Sprint Contract: [docs/contracts/player.md](docs/contracts/player.md) + - 의존: recorder 산출물 포맷 확정 +6. **diff-reporter PoC** — Sprint Contract: [docs/contracts/diff-reporter.md](docs/contracts/diff-reporter.md) + - 의존: normalizer 규칙 1개 이상 + +## P2 — 통합 + +7. **test-runner** — 시나리오 일괄 실행 + 실패 triage + - 의존: recorder + player + normalizer + diff-reporter 전부 +8. **engine-bridge 탐색** — HmEG PDB 리플렉션으로 카메라/선택 상태 접근 가능 여부 확인 + - Sprint Contract 필요 +9. **AutomationPeer PR 전략 PoC** — SUT fork에 AI로 AutomationPeer 자동 부착 샘플 + +## P3 — 상시 + +- **Scaffolding review** (PoC 3개마다) +- **.claude/ 비계 감사** — 기사 원칙: 불필요한 비계 제거 +- **history 파일 누락 체크** — Stop hook이 경고 + +## 규칙 + +- 항목을 집을 때 PROGRESS.md의 "In progress"에 기록하고 본인(에이전트명/세션) 명시. +- 비자명한 P0/P1/P2 항목은 반드시 Sprint Contract 먼저. +- Generator와 Evaluator는 같은 세션이 겸하지 않는다. +- 완료 시: evaluator pass → PROGRESS.md Done 이동 → PLAN.md 제거 → history 작성 → `/handoff`. diff --git a/PROGRESS.md b/PROGRESS.md index 1584163..72313d3 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,17 +1,44 @@ # PROGRESS.md — recordingtest -## 현재 상태 요약 +> 에이전트 간 공유 메모리. 세션 시작 시 PLAN.md와 함께 반드시 읽을 것. +> 작업 상태만 기록한다. 결정의 근거는 `docs/history/`, 전략은 `CLAUDE.md` / 메모리 참조. -- 프로젝트: WPF Application User Input Regression Test -- 저장소: https://gitea.hmac.kr/kimminsung/recordingtest -- 히스토리 경로: docs/history +**저장소**: https://gitea.hmac.kr/kimminsung/recordingtest +**SUT**: `EG-BIM Modeler/` (git 제외, 로컬 동봉) +**현재 이슈**: #2 컨텍스트 엔지니어링 & AI 개발환경 셋업 -## 작업 이력 +--- -| 날짜 | 작업 | 이슈 | 파일 | -|------|------|------|------| +## Done -## 참고 +| 날짜 | 항목 | 산출물 | +|------|------|--------| +| 2026-04-07 | 히스토리 훅 설정 | `docs/history/2026-04-07_히스토리-훅-설정.md` | +| 2026-04-07 | 이슈 #2 리서치 & 에이전트 분해 | 메모리 `project_recordingtest_plan.md` | +| 2026-04-07 | 구현 고려사항 정리 (8개 카테고리) | 히스토리 | +| 2026-04-07 | Golden-file 회귀 전략 채택 | 메모리 `project_recordingtest_strategy.md` | +| 2026-04-07 | CLAUDE.md 초안 + §0 협업 규칙 | `CLAUDE.md` | +| 2026-04-07 | SUT(EG-BIM Modeler) 정적 분석 | 메모리 `project_sut_egbim_modeler.md` | +| 2026-04-07 | `.claude/` agents·commands·skills·hooks 셋업 | `.claude/` | +| 2026-04-07 | Harness design 채택 (Planner/Generator/Evaluator) | CLAUDE.md §0.1, `.claude/agents/planner.md`, `evaluator.md` | +| 2026-04-07 | SUT 폴더 `.gitignore` | `.gitignore` | +| 2026-04-07 | 초기 Sprint Contracts 5건 작성 | `docs/contracts/*.md` | +| 2026-04-07 | SUT 카탈로그 v0 (정적) | `docs/sut-catalog/catalog.md`, `plugins.md` | +| 2026-04-07 | 솔루션 스캐폴드(sut-prober PoC 타깃) | `recordingtest.sln`, `src/Recordingtest.SutProber/` | -- 히스토리 파일은 `docs/history/YYYY-MM-DD_{작업명}.md` 형식으로 저장 -- 필수 항목: **소요 시간**, **Context 사용량**, **이슈** (#N) +## In progress + +_(없음 — 다음 작업은 PLAN.md 상단에서 고른다)_ + +## Blocked + +_(없음)_ + +## Recent regression runs + +_(러너 미구현 — /regress 사용 불가)_ + +## Scaffolding review + +- 마지막 감사: 2026-04-07 (초기 셋업) +- 다음 감사 권장: PoC 3개 완료 후 diff --git a/docs/contracts/diff-reporter.md b/docs/contracts/diff-reporter.md new file mode 100644 index 0000000..7d14935 --- /dev/null +++ b/docs/contracts/diff-reporter.md @@ -0,0 +1,44 @@ +# Sprint Contract — diff-reporter + +**Owner:** Generator +**Depends on:** normalizer (정규화된 입력) +**Issue:** #2 + +## Goal + +`*.approved`와 `*.received` 쌍을 받아 의미 있는 diff를 생성하고, 실패 지점을 시각화한다. `diff-triager` 서브에이전트가 읽을 수 있는 구조화 출력도 제공. + +## Definition of Done + +- [ ] `Recordingtest.DiffReporter` 라이브러리 + CLI +- [ ] 입력: `--approved --received --out ` +- [ ] JSON/텍스트 파일은 라인·객체 단위 의미 diff, 바이너리는 hex 요약 diff +- [ ] 출력: `diff.json`(구조화), `diff.md`(사람용), `diff.html`(옵션, side-by-side) +- [ ] `diff.json` 스키마: `{ file, hunks[], summary: { added, removed, changed } }` +- [ ] 동일 파일 비교 시 "identical"로 단정, exit code 0 +- [ ] 차이 존재 시 exit code 1 + 요약을 stdout 1줄 +- [ ] diff-triager 에이전트가 `diff.json`만 읽어 bucket 분류 가능 (통합 테스트 1개) + +## Interfaces + +- **Inputs:** 두 파일 경로 +- **Outputs:** `diff.json`, `diff.md`, (옵션) `diff.html` +- **Side effects:** 없음 + +## Out of scope + +- 정규화 자체 (normalizer) +- triage 분류 로직 (diff-triager 에이전트) +- 자동 approve (`/approve` 커맨드) + +## Evaluation plan + +1. 동일 파일 2개 → identical 판정 +2. 1필드만 바뀐 JSON → `diff.json.hunks.length == 1` +3. 바이너리 파일 diff → hex summary 출력 +4. diff-triager에 통합: normalization gap 케이스에서 bucket=`normalization_gap` 분류 확인 + +## Risks + +- 큰 파일(수십 MB 모델)에서 성능 — 스트리밍 diff 필요 +- JSON 스키마 변경 시 triager 하위 호환성 diff --git a/docs/contracts/normalizer.md b/docs/contracts/normalizer.md new file mode 100644 index 0000000..c40da9f --- /dev/null +++ b/docs/contracts/normalizer.md @@ -0,0 +1,43 @@ +# Sprint Contract — normalizer + +**Owner:** Generator +**Depends on:** sut-prober (Json 카탈로그) +**Issue:** #2 + +## Goal + +SUT 저장 파일(Json 설정 + 모델 파일)의 비결정적 필드를 정규화해 golden-file 비교를 결정적으로 만든다. 규칙은 **프로파일 단위로 버전 관리**되고 단위 테스트를 동반한다. + +## Definition of Done + +- [ ] `Recordingtest.Normalizer` 라이브러리가 `Normalize(input, profile)` API 제공 +- [ ] 기본 프로파일 `default` 포함 규칙 최소 5개: 타임스탬프 마스킹, GUID 치환, 절대 경로 → ``/``, 부동소수점 epsilon(1e-6), JSON 객체 키 정렬 +- [ ] 프로파일은 `profiles/*.yaml`로 선언, 코드 변경 없이 규칙 추가 가능 +- [ ] 각 규칙마다 `tests/Normalizer.Tests/` 아래 before/after 샘플 1쌍 이상 +- [ ] 동일 입력으로 두 번 정규화해도 같은 바이트 출력 (idempotent) +- [ ] 정규화 로그(sidecar) `normalization.log` 생성 — 어떤 규칙이 몇 번 적용됐는지 기록 +- [ ] `json-configs.json` 카탈로그의 "suspected fields" 전수를 default 프로파일로 덮는다 + +## Interfaces / contracts + +- **Inputs:** 파일 경로 + 프로파일 이름 +- **Outputs:** 정규화된 바이트 스트림 + sidecar 로그 +- **Side effects:** sidecar 로그 쓰기만 + +## Out of scope + +- 바이너리 `.hme`/`.egm` 파서 (별도 contract) +- SUT 내부 호출 +- 정규화 규칙 자동 학습 + +## Evaluation plan + +1. `dotnet test tests/Normalizer.Tests` 전부 pass +2. `json-configs.json`의 모든 suspected field가 default 프로파일 규칙에 커버됨 (매핑 테이블 검증) +3. 동일 입력 2회 정규화 바이트 diff 없음 +4. 샘플 `Settings.json` 정규화 전/후 비교에서 타임스탬프·경로가 마스킹됨 + +## Risks + +- epsilon 선택이 SUT 실제 정밀도와 충돌 가능 → 구성 가능해야 함. +- 컬렉션 순서 정렬이 SUT 의미를 바꿀 수 있음 → 정렬 대상은 명시적 화이트리스트. diff --git a/docs/contracts/player.md b/docs/contracts/player.md new file mode 100644 index 0000000..09dc1d9 --- /dev/null +++ b/docs/contracts/player.md @@ -0,0 +1,44 @@ +# Sprint Contract — player + +**Owner:** Generator +**Depends on:** recorder 스키마 확정 +**Issue:** #2 + +## Goal + +recorder가 생성한 시나리오 yaml을 SUT에 재생해 결과 저장 파일을 생성한다. 타이밍은 고정 sleep 금지, UIA/이벤트 기반 동기화. + +## Definition of Done + +- [ ] `Recordingtest.Player` 콘솔이 `--scenario [--output-dir ]` 받음 +- [ ] 각 step 재생 전 `wait_for`(uia 이벤트/엘리먼트 property) 대기 구현 +- [ ] `target.uia_path`로 엘리먼트 재탐색 후 `offset_norm`으로 좌표 계산해 입력 전달 +- [ ] 재생 중 예외 발생 시 시나리오 스텝 index + 엘리먼트 탐색 실패 이유를 아티팩트로 기록 +- [ ] 체크포인트 step에서 현재 저장 파일을 `/checkpoint-.*`로 복사 +- [ ] 재생 완료 시 exit code 0, 실패 시 non-zero + 아티팩트 경로 출력 +- [ ] 동일 시나리오 10회 재생 시 9회 이상 성공 (flaky 허용 상한) + +## Interfaces + +- **Inputs:** 시나리오 yaml, 아티팩트 출력 디렉터리 +- **Outputs:** checkpoint 저장 파일, 실패 아티팩트(스크린샷, UIA 덤프, 로그) +- **Side effects:** SUT 프로세스 실행/종료 + +## Out of scope + +- 정규화 (normalizer의 몫) +- diff (diff-reporter의 몫) +- 리포트 집계 (test-runner의 몫) + +## Evaluation plan + +1. recorder로 만든 간단 시나리오(Box 생성 저장)를 10회 재생 → 9회 이상 완료 파일 생성 +2. 의도적으로 잘못된 `uia_path`를 넣어 실패 → 아티팩트에 탐색 실패 이유 기록 확인 +3. 체크포인트 2개 시나리오 재생 → 각 checkpoint 파일 존재 확인 +4. 고정 sleep 호출 grep으로 0건 확인 + +## Risks + +- plugin 로드 지연 → 첫 step 전 "plugin ready" 신호 필요 +- 시작 상태 초기화 (임시 프로젝트 파일 삭제) 미비 시 누적 오염 +- 재생 중 모달 대화상자 출현 시 대응 정책 필요 diff --git a/docs/contracts/recorder.md b/docs/contracts/recorder.md new file mode 100644 index 0000000..7765e3e --- /dev/null +++ b/docs/contracts/recorder.md @@ -0,0 +1,47 @@ +# Sprint Contract — recorder + +**Owner:** Generator +**Depends on:** FlaUI 승인, sut-prober(UIA 힌트용 카탈로그) +**Issue:** #2 + +## Goal + +사용자가 EG-BIM Modeler에서 수동 테스트하는 동안 입력(키·마우스·창 이벤트)을 캡처해 element-aware 시나리오 파일을 생성한다. + +## Definition of Done + +- [ ] `Recordingtest.Recorder` 콘솔 실행 시 SUT에 attach하고 입력 캡처 시작 +- [ ] 캡처되는 이벤트: 키 다운/업, 마우스 클릭/드래그/휠, 포커스 변경 +- [ ] 각 이벤트는 `{ ts, kind, uia_path, offset_norm, raw_coord, value }` 형식으로 기록 +- [ ] 3D 뷰포트(SharpDX surface) 클릭 시 uia_path는 호스팅 엘리먼트, `offset_norm`은 `[0..1]` 정규화 좌표 +- [ ] 출력: `scenarios/.yaml` 스키마 준수 (`scenario-author` 규격 일치) +- [ ] 비밀번호/토큰 마스킹 규칙 1개 이상 (예: focus된 element가 `PasswordBox`이면 `value`를 ``) +- [ ] 캡처 중 SUT 성능에 눈에 띄는 영향 없음 (60 FPS 유지; 경고만) +- [ ] 캡처 종료 시 요약(이벤트 수, 소요 시간, uia_path 미결 건수) + +## Interfaces + +- **Inputs:** `--output scenarios/.yaml`, `--attach ` +- **Outputs:** 시나리오 yaml +- **Side effects:** 전역 Win32 hook 등록/해제 + +## Out of scope + +- 리플레이 (player의 몫) +- 3D 엔진 상태 캡처 (engine-bridge 사용 예정) +- 자동 이름 추정 / 자연어 변환 (scenario-author) + +## Evaluation plan + +1. SUT를 수동 실행 후 recorder attach → Box 명령 버튼 클릭 → Box 생성 드래그 → 저장 +2. 출력 yaml 파싱 + 스키마 검증 +3. 클릭 이벤트의 `uia_path`가 `Button[@Name*='Box']` 형태 포함 확인 +4. 드래그 이벤트의 `offset_norm`이 `[0..1]` 범위 확인 +5. `PasswordBox`에 더미 입력 → `` 기록 확인 +6. 종료 후 hook 해제 확인 (재실행 시 double-hook 안 남음) + +## Risks + +- IME(한글) 조합 키 이벤트 손실 → 클립보드 우회 필요 가능 +- 해상도/DPI 변경 시 `raw_coord`는 유효성 상실 → `offset_norm` 우선 사용 +- Win32 hook은 UI thread 블록 가능 → 별도 스레드 + 큐 필수 diff --git a/docs/contracts/sut-prober.md b/docs/contracts/sut-prober.md new file mode 100644 index 0000000..179b2a7 --- /dev/null +++ b/docs/contracts/sut-prober.md @@ -0,0 +1,48 @@ +# Sprint Contract — sut-prober + +**Owner:** Generator (일반 세션) +**Depends on:** 없음 +**Issue:** #2 + +## Goal + +EG-BIM Modeler SUT의 **정적** 구조(플러그인·설정·어셈블리)를 덤프해 `docs/sut-catalog/`에 베이스라인 가능한 카탈로그를 생성한다. SUT는 실행하지 않는다. + +## Definition of Done + +- [ ] `dotnet run --project src/Recordingtest.SutProber` 실행으로 카탈로그 3종이 생성된다: `plugins.json`, `json-configs.json`, `assemblies.json` +- [ ] `plugins.json`은 `EG-BIM Modeler/Plugins/Eg*Plugin/` 187개(±) 전부를 담고, 각 항목은 `{ name, path, dlls[], size_bytes }` 형식 +- [ ] `json-configs.json`은 `EG-BIM Modeler/Json/*.json` 각 파일별 `{ name, top_level_keys[], suspected_nondeterministic_fields[] }` +- [ ] `assemblies.json`은 `HmEG*.dll`, `Editor*.dll`, `HmGeometry*.dll`의 `{ name, size, has_pdb }` +- [ ] 출력은 **정렬되어 있어** 두 번 실행해도 동일 (decimal/텍스트 diff 없음) +- [ ] `EG-BIM Modeler/` 폴더에 대한 쓰기 접근이 없다 (코드 리뷰로 확인) +- [ ] 경로는 repo root 상대 경로로 기록(절대 경로 금지) + +## Interfaces / contracts + +- **Inputs:** `EG-BIM Modeler/` (read-only), CLI 인자 `--sut ` (기본 `EG-BIM Modeler`) +- **Outputs:** `docs/sut-catalog/plugins.json`, `docs/sut-catalog/json-configs.json`, `docs/sut-catalog/assemblies.json` +- **Side effects:** 없음 (로그 제외) + +## Out of scope + +- SUT 실행 / 리플렉션 / 동적 분석 +- PDB 파싱 (engine-bridge의 몫) +- UIA 트리 덤프 (recorder 또는 별도 PoC의 몫) + +## Evaluation plan + +evaluator는 다음을 수행: + +1. `dotnet build` 성공 +2. `dotnet run --project src/Recordingtest.SutProber -- --sut "EG-BIM Modeler"` 실행, exit code 0 확인 +3. 세 카탈로그 파일 존재 확인 + JSON 파싱 유효성 +4. `plugins.json` 엔트리 수 ≥ 180 +5. 두 번째 실행 후 세 파일 `git diff` 결과가 비어있음을 확인 (결정성) +6. `grep -r "EG-BIM Modeler" src/Recordingtest.SutProber/` 결과에 `File.Write|File.Delete|File.Create` 호출 없음 확인 + +## Risks / open questions + +- .NET 버전 합의 필요 (`net8.0` 제안). SUT와 다를 수 있으나 prober는 독립 프로세스이므로 무관. +- 플러그인 폴더 구조가 일정한지 샘플 확인 필요. +- 경로 구분자(Windows `\`) 정규화 정책 — 출력에서 `/`로 통일 권장. diff --git a/docs/history/2026-04-07_이슈2-AI-AutomationPeer-부착옵션.md b/docs/history/2026-04-07_이슈2-AI-AutomationPeer-부착옵션.md new file mode 100644 index 0000000..7886a03 --- /dev/null +++ b/docs/history/2026-04-07_이슈2-AI-AutomationPeer-부착옵션.md @@ -0,0 +1,19 @@ +# 2026-04-07 이슈 #2 — AI 기반 AutomationPeer 부착 옵션 기록 + +- **이슈**: kimminsung/recordingtest#2 +- **소요 시간**: ~3분 +- **Context 사용량**: ~70k tokens + +## 결정/기록 + +SUT 커스텀 컨트롤에 AutomationPeer를 부착하는 작업을 AI로 자동화하는 보조 전략을 메모리에 남김. Golden-file이 1차 전략이지만, element-aware 레코딩 보강이 필요해질 때 꺼낼 카드. + +### 핵심 +- 패턴 정형화로 80% AI 자동 생성 가능 +- ControlType/패턴 매핑과 `AutomationProperties.Name`은 사람 리뷰 필요 +- SUT 저장소엔 별도 브랜치/PR로 제안, 직접 커밋 금지 +- recordingtest 저장소에는 가이드/스크립트만, SUT 코드 변경은 fork/PR로 분리 + +## 산출물 +- 메모리: `project_recordingtest_automationpeer_ai.md` +- MEMORY.md 인덱스 갱신 diff --git a/docs/history/2026-04-07_이슈2-CLAUDE-md-progress-plan-규칙.md b/docs/history/2026-04-07_이슈2-CLAUDE-md-progress-plan-규칙.md new file mode 100644 index 0000000..50b907d --- /dev/null +++ b/docs/history/2026-04-07_이슈2-CLAUDE-md-progress-plan-규칙.md @@ -0,0 +1,21 @@ +# 2026-04-07 이슈 #2 — CLAUDE.md에 PROGRESS/PLAN 협업 규칙 추가 + +- **이슈**: kimminsung/recordingtest#2 +- **소요 시간**: ~3분 +- **Context 사용량**: ~76k tokens + +## 변경 내용 + +`CLAUDE.md` 에 §0 "세션 시작 시 필독" 섹션 신설. 여러 에이전트 협업을 위한 공유 메모리로 `PROGRESS.md`(끝난 일)와 `PLAN.md`(할 일) 운영 규칙 명시. + +### 핵심 규칙 +- 세션 시작: PROGRESS.md + PLAN.md 먼저 read +- 시작 시: PROGRESS.md에 in progress 표시 +- 종료 시: PROGRESS.md(done 이동) + PLAN.md(완료 제거/다음 단계) + history 파일 (3종 세트) +- 충돌 시 최신 커밋 기준 머지, 모호하면 사용자에 질문 + +§7 Claude 작업 원칙에도 같은 내용 반영. + +## 다음 단계 + +PROGRESS.md / PLAN.md 실제 파일은 셋업 단계에서 작성 예정 (현재는 CLAUDE.md 규칙만 정의). diff --git a/docs/history/2026-04-07_이슈2-CLAUDE-md-작성.md b/docs/history/2026-04-07_이슈2-CLAUDE-md-작성.md new file mode 100644 index 0000000..305c8f2 --- /dev/null +++ b/docs/history/2026-04-07_이슈2-CLAUDE-md-작성.md @@ -0,0 +1,26 @@ +# 2026-04-07 이슈 #2 — CLAUDE.md 작성 + +- **이슈**: kimminsung/recordingtest#2 +- **소요 시간**: ~5분 +- **Context 사용량**: ~63k tokens + +## 작업 내용 + +프로젝트 셋업 전, recordingtest(WPF 입력 회귀 자동화 도구)의 운영 지침인 `CLAUDE.md`를 루트에 작성. 셋업은 미실시. + +## 포함 섹션 + +1. 프로젝트 정체성 / SUT 개요 +2. 핵심 전략 — Golden-file 회귀 +3. 아키텍처 구성요소(8개 모듈) 우선순위 +4. 기술 스택 가이드 (FlaUI, Win32 hook, JSON/YAML, LFS) +5. 설계 원칙 8개 (결정성, element-aware, 타이밍, marshaling, 체크포인트, 아티팩트, 침습 최소화, 마스킹) +6. 환경 제약 (세션0, DPI, GPU) +7. 작업 흐름 규칙 (히스토리 필수) +8. 디렉터리 구조(예정) +9. 비목표 +10. 결정 로그 위치 + +## 다음 단계 + +사용자 승인 후 실제 솔루션/프로젝트 셋업 진행. diff --git a/docs/history/2026-04-07_이슈2-SUT-EGBIM-분석.md b/docs/history/2026-04-07_이슈2-SUT-EGBIM-분석.md new file mode 100644 index 0000000..b03548c --- /dev/null +++ b/docs/history/2026-04-07_이슈2-SUT-EGBIM-분석.md @@ -0,0 +1,28 @@ +# 2026-04-07 이슈 #2 — SUT (EG-BIM Modeler) 폴더 분석 + +- **이슈**: kimminsync/recordingtest#2 +- **소요 시간**: ~5분 +- **Context 사용량**: ~82k tokens + +## 분석 대상 + +`d:/MYCLAUDE_PROJECT/recordingtest/EG-BIM Modeler/` — SUT 실행파일 일습. + +## 핵심 발견 + +- WPF + .NET, 3D 엔진 `HmEG.dll`, 렌더 SharpDX D3D11 +- MEF plugin `Plugins/Eg*Plugin/` 수십~100개+ +- **PDB 동봉** → 리플렉션/엔진 침습 비용 ↓ +- `Json/` 폴더에 텍스트 설정 다수 — golden-file 1차 타깃으로 적합 +- 자체 저장 포맷 `.hme`/`.egm` — 파서·정규화 규칙 필요 +- 3D 뷰포트는 D3D surface로 UIA 사각지대 확정 + +## 전략 반영 + +- sut-prober 첫 PoC: `Plugins/` 스캔 + `Json/` 스냅샷 +- engine-bridge: HmEG 리플렉션으로 카메라/씬그래프 접근 +- 모델 포맷 파서 모듈 분리 + +## 산출물 +- 메모리: `project_sut_egbim_modeler.md` +- MEMORY.md 인덱스 갱신 diff --git a/docs/history/2026-04-07_이슈2-claude-agents-commands-skills-hooks-셋업.md b/docs/history/2026-04-07_이슈2-claude-agents-commands-skills-hooks-셋업.md new file mode 100644 index 0000000..6f66dc6 --- /dev/null +++ b/docs/history/2026-04-07_이슈2-claude-agents-commands-skills-hooks-셋업.md @@ -0,0 +1,68 @@ +# 2026-04-07 이슈 #2 — .claude/ 에이전트·커맨드·스킬·훅 셋업 + +- **이슈**: kimminsung/recordingtest#2 +- **소요 시간**: ~15분 +- **Context 사용량**: ~95k tokens + +## 작업 내용 + +Claude Code 공식 포맷 조사(claude-code-guide 에이전트) 후, recordingtest 운영에 필요한 커스터마이즈를 `.claude/` 하위에 생성. + +### Subagents — `.claude/agents/*.md` +- **sut-explorer** — EG-BIM Modeler 정적 분석(플러그인/Json/어셈블리). SUT 실행 금지. +- **diff-triager** — 회귀 실패 분류(real bug / intentional / normalization gap / env drift / flaky). +- **scenario-author** — 자연어 → 시나리오 YAML 생성. + +### Slash commands — `.claude/commands/*.md` +- **/sut-probe** — sut-explorer 위임, 카탈로그 생성. +- **/regress** — 회귀 실행 + 실패 자동 triage. +- **/approve** — received → approved 승격, 사유 강제. +- **/handoff** — PROGRESS/PLAN/history 3종 세트 갱신. +- **/progress** — PROGRESS.md/PLAN.md 요약 출력. + +### Skills — `.claude/skills//SKILL.md` +- **golden-file-normalizer** — 정규화 규칙 카테고리/원칙/저작 가이드. +- **flaui-cookbook** — FlaUI 런칭/대기/엘리먼트 캡처/3D 뷰포트 폴백/함정. + +### Hooks — `.claude/settings.json` + `.claude/hooks/*.sh` +- **SessionStart** → `session-start-progress.sh`: PROGRESS.md/PLAN.md 자동 주입. +- **PreToolUse(Bash)** → `guard-sut-launch.sh`: EG-BIM Modeler.exe 실행 경고. +- **PreToolUse(Edit|Write)** → `guard-sut-folder.sh`: SUT 폴더 수정 차단(exit 2). +- **Stop** → `stop-handoff-reminder.sh`: 오늘 history 파일/PROGRESS/PLAN 누락 시 /handoff 권장. + +## 원칙 + +- 포맷은 claude-code-guide 조사 결과를 따름(frontmatter: name/description/allowed-tools/tools/model). +- SUT 폴더는 **read-only** — 훅으로 강제. +- 실제 모듈(src/*)은 아직 미생성 — /regress 등은 건드릴 파일이 없으면 early-exit. + +## 산출물 + +``` +.claude/ +├── agents/ +│ ├── sut-explorer.md +│ ├── diff-triager.md +│ └── scenario-author.md +├── commands/ +│ ├── sut-probe.md +│ ├── regress.md +│ ├── approve.md +│ ├── handoff.md +│ └── progress.md +├── skills/ +│ ├── golden-file-normalizer/SKILL.md +│ └── flaui-cookbook/SKILL.md +├── hooks/ +│ ├── session-start-progress.sh +│ ├── guard-sut-launch.sh +│ ├── guard-sut-folder.sh +│ └── stop-handoff-reminder.sh +└── settings.json (hooks 섹션 추가) +``` + +## 다음 단계 + +- PROGRESS.md / PLAN.md 실제 파일 작성 (셋업 단계에서) +- src/ 모듈 scaffold +- 훅 bash 스크립트 실제 동작 검증 diff --git a/docs/history/2026-04-07_이슈2-golden-file-전략-채택.md b/docs/history/2026-04-07_이슈2-golden-file-전략-채택.md new file mode 100644 index 0000000..b3da308 --- /dev/null +++ b/docs/history/2026-04-07_이슈2-golden-file-전략-채택.md @@ -0,0 +1,34 @@ +# 2026-04-07 이슈 #2 — Golden-file 회귀 전략 채택 + +- **이슈**: kimminsung/recordingtest#2 +- **소요 시간**: ~5분 +- **Context 사용량**: ~58k tokens + +## 결정 + +사용자 제안 채택: **수동 테스트 입력 레코딩 → 리플레이 후 저장 파일을 베이스라인과 diff** 하는 golden-file 회귀 방식을 1차 검증 수단으로 삼는다. (ApprovalTests 패턴과 동형) + +## 근거 + +- SUT(WPF+MEF+HmEG)에 AutomationPeer/probe 부착 협조 비용 회피 +- 결과물 파일 자체가 검증 기준 → 직관적, 사용자가 시나리오 작성 가능 +- 픽셀 비교 의존도 ↓ → viewport-verifier 우선순위 하향 + +## 성립 조건 / 함정 + +1. **결정성**: 저장 파일의 타임스탬프/GUID/경로/부동소수점/순서 → 정규화 파이프라인 필수 +2. **입력 재현 정확도**: 타이밍·포커스·IME·3D 드래그 픽셀 증폭 +3. **비교 범위**: 저장 파일만 vs +중간 체크포인트 vs +엔진 상태 sidecar +4. **베이스라인 관리**: `*.approved`/`*.received` 워크플로, 큰 파일 LFS +5. **진단**: 단계별 스냅샷으로 갈라진 지점 이분 탐색 + +## 모듈 우선순위 재조정 + +핵심: **recorder + player + 정규화 모듈 + diff-reporter** +보조: engine-bridge(sidecar JSON 덤프 용도) +후순위: viewport-verifier(픽셀 비교) + +## 다음 단계 + +- 저장 파일 포맷 조사 (텍스트/바이너리, 정규화 가능 영역) +- recorder PoC 범위 정의 diff --git a/docs/history/2026-04-07_이슈2-harness-design-planner-evaluator-도입.md b/docs/history/2026-04-07_이슈2-harness-design-planner-evaluator-도입.md new file mode 100644 index 0000000..8990242 --- /dev/null +++ b/docs/history/2026-04-07_이슈2-harness-design-planner-evaluator-도입.md @@ -0,0 +1,41 @@ +# 2026-04-07 이슈 #2 — Planner/Evaluator 에이전트 & Sprint Contract 도입 + +- **이슈**: kimminsung/recordingtest#2 +- **소요 시간**: ~10분 +- **Context 사용량**: ~108k tokens + +## 배경 + +Anthropic "Harness Design for Long-Running Agent Applications" 기사를 읽고, 핵심 원칙을 recordingtest에 반영. + +## 추가된 것 + +### 에이전트 +- `.claude/agents/planner.md` — 요청 → Sprint Contract + PLAN.md 엔트리. 구현 금지. +- `.claude/agents/evaluator.md` — 완료된 모듈을 Sprint Contract 기준으로 독립 채점. 자기 작업 평가 금지. + +### 커맨드 +- `.claude/commands/contract.md` — `/contract ` +- `.claude/commands/evaluate.md` — `/evaluate ` + +### CLAUDE.md +- §0.1 "작업 사이클 — Planner → Generator → Evaluator" 섹션 신설 +- Sprint Contract / DoD 객관성 / 독립 평가 원칙 명시 +- 컨텍스트 위생: compaction 대신 reset, Stop hook 경고 준수, scaffolding audit 상시 항목 +- 디렉터리 구조에 `docs/contracts/`, `PROGRESS.md`, `PLAN.md` 추가 + +### 메모리 +- `project_recordingtest_harness_design.md` 신규 +- MEMORY.md 인덱스 갱신 + +## 원칙 요약 + +1. 생성자와 평가자는 같은 에이전트가 겸하지 않는다. +2. DoD는 객관적으로 검증 가능해야 한다. +3. 컨텍스트가 차면 요약하지 말고 파일에 쏟고 리셋. +4. evaluator fail → PROGRESS.md에 done으로 넘기지 않는다. +5. `.claude/` 비계는 주기적으로 감사·축소. + +## 다음 단계 + +PROGRESS.md / PLAN.md 실제 부트스트랩 (첫 번째 Sprint Contract 작성부터). diff --git a/docs/history/2026-04-07_이슈2-구현고려사항-정리.md b/docs/history/2026-04-07_이슈2-구현고려사항-정리.md new file mode 100644 index 0000000..471d578 --- /dev/null +++ b/docs/history/2026-04-07_이슈2-구현고려사항-정리.md @@ -0,0 +1,26 @@ +# 2026-04-07 이슈 #2 — 구현 고려사항 정리 + +- **이슈**: kimminsung/recordingtest#2 +- **소요 시간**: ~5분 +- **Context 사용량**: ~52k tokens + +## 정리한 고려사항 카테고리 + +1. **레코딩/리플레이 안정성** — 좌표 vs element, 타이밍, 포커스/Z-order, IME +2. **WPF/MEF 특성** — Custom AutomationPeer, plugin 비동기 로드, Dispatcher marshaling +3. **3D 뷰포트(HmEG) 검증** — 픽셀 비교 한계, 엔진 내부 상태 검증, 픽킹 안정성 +4. **환경/재현성** — DPI, 멀티모니터, 시계/랜덤 격리, CI 세션 0 제약 +5. **테스트 자산 관리** — JSON/YAML 시나리오, 베이스라인 이미지 LFS, 모듈화 +6. **진단/디버깅** — 실패 아티팩트(스샷·UIA 덤프·엔진 상태), 영상 녹화, diff 오버레이 +7. **보안/안전** — 입력 마스킹, 권한 일치 +8. **거버넌스** — SUT 코드 변경 협조(AutomationPeer/probe), 유지보수 비용 + +## 핵심 인사이트 + +- 픽셀 비교보다 **엔진 내부 상태(engine-bridge)** 가 훨씬 안정적 → 우선순위 상향 +- **SUT 팀 협조** 가 기술적 난제보다 큰 장애물 (AutomationPeer/probe 부착) +- WPF는 세션 0에서 못 돌므로 CI는 대화형 세션 agent 필요 + +## 다음 단계 + +위 고려사항을 sut-prober PoC 설계에 반영. diff --git a/docs/history/2026-04-07_이슈2-리서치-에이전트분해.md b/docs/history/2026-04-07_이슈2-리서치-에이전트분해.md new file mode 100644 index 0000000..38dba7e --- /dev/null +++ b/docs/history/2026-04-07_이슈2-리서치-에이전트분해.md @@ -0,0 +1,41 @@ +# 2026-04-07 이슈 #2 — 리서치 및 Agent 분해 + +- **이슈**: kimminsung/recordingtest#2 — 요구사항 컨텍스트 엔지니어링과 AI 개발환경 셋팅 +- **소요 시간**: ~15분 +- **Context 사용량**: ~45k tokens + +## 작업 내용 + +WPF + MEF 플러그인 + 자체 HmEG 3D 엔진으로 구성된 SUT를 대상으로 한 입력 회귀 테스트 자동화 도구의 접근법을 웹 리서치하고, 단계별 agent로 분해하여 메모리에 저장. + +### 리서치 결론 + +- **UIA 1순위**: FlaUI (.NET native, WPF 정밀 제어). WinAppDriver는 정체, Appium은 그 래퍼. +- **3D 뷰포트**: UIA 불가 → 좌표 입력 + 픽셀/이미지 비교 + 엔진 in-process hook 하이브리드. +- **MEF plug-in**: 각 plugin 프로젝트에 custom `AutomationPeer` 부착, plugin 상태를 probe로 노출. +- **레코딩**: Win32 low-level hook + UIA element path + offset 동시 저장(해상도 내성). + +### Agent 분해(예정) + +1. sut-prober — UIA 트리 덤프, MEF plugin 목록 +2. recorder — element-aware 입력 캡처 +3. player — 재생/동기화 +4. viewport-verifier — 3D 스크린샷 비교 +5. engine-bridge — HmEG 상태 노출 +6. test-runner — 시나리오 실행/리포트 +7. diff-reporter — 실패 시각화 + +## 산출물 + +- 메모리: `project_recordingtest_goal.md`, `project_recordingtest_plan.md` +- 본 히스토리 파일 + +## 다음 단계 + +`sut-prober` PoC부터 시작 — 사용자 결정 대기. + +## 참고 링크 + +- [FlaUI vs WinAppDriver 2026](https://www.testsprite.com/use-cases/en/the-most-accurate-alternatives-to-winappdriver) +- [WPF Custom AutomationPeer](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/ui-automation-of-a-wpf-custom-control) +- [TestComplete WPF Record/Replay](https://support.smartbear.com/testcomplete/docs/app-testing/desktop/wpf/index.html) diff --git a/docs/history/2026-04-07_이슈2-병렬-스캐폴드-contracts.md b/docs/history/2026-04-07_이슈2-병렬-스캐폴드-contracts.md new file mode 100644 index 0000000..86d8ac1 --- /dev/null +++ b/docs/history/2026-04-07_이슈2-병렬-스캐폴드-contracts.md @@ -0,0 +1,51 @@ +# 2026-04-07 이슈 #2 — 병렬 스캐폴드 + Sprint Contracts 일괄 + +- **이슈**: kimminsung/recordingtest#2 +- **소요 시간**: ~20분 +- **Context 사용량**: ~130k tokens + +## 작업 내용 + +"병렬화 가능한 부분 병렬 구현" 요청에 따라, 독립 파일들을 한 세션에서 병렬 Write로 일괄 생성. + +### 부트스트랩 문서 +- `PROGRESS.md` — 공유 상태 파일 확장 (Done/In progress/Blocked/Scaffolding review) +- `PLAN.md` — P0~P3 우선순위 큐 + 작업 규칙 + +### Sprint Contracts (5건) +`docs/contracts/` 하위: +- `sut-prober.md` — 187개 플러그인 카탈로그 + json-configs + assemblies 덤프 +- `normalizer.md` — 기본 프로파일 5규칙, idempotent +- `recorder.md` — element-aware 입력 캡처, IME·마스킹 포함 +- `player.md` — 10회 중 9회 성공, 고정 sleep 금지 +- `diff-reporter.md` — JSON/텍스트 의미 diff, triager 연동 + +각 contract는 planner 에이전트 규격(Goal / DoD / Interfaces / Out of scope / Evaluation plan / Risks)을 준수하며, DoD 항목은 모두 객관적으로 검증 가능. + +### SUT 카탈로그 v0 +- `docs/sut-catalog/catalog.md` — 어셈블리·UI스택·Json 설정·저장 포맷·영향 요약 +- `docs/sut-catalog/plugins.md` — 187개 플러그인 전체 목록 (사전순) + +### 솔루션 스캐폴드 (sut-prober PoC) +- `recordingtest.sln` +- `Directory.Build.props` (net8.0, nullable, warnings-as-errors) +- `src/Recordingtest.SutProber/` 4개 파일: + - `Recordingtest.SutProber.csproj` + - `Program.cs` — CLI 진입, `--sut`/`--out` 인자 + - `PluginScanner.cs` — 정렬된 플러그인 엔트리 스캔 + - `JsonConfigScanner.cs` — Json/*.json top-level 키 + 의심 필드 휴리스틱 + - `AssemblyScanner.cs` — HmEG*/Editor*/HmGeometry* 어셈블리 + PDB 존재 + +**주의**: sut-prober 구현은 contract를 **선제적으로 맞추지는 않음** — 빌드·실행·evaluator 검증은 별도 Generator 세션의 몫. 현재는 컴파일 가능한 뼈대 수준. + +## 병렬화 전략 + +- 파일 간 의존성 0인 것들만 병렬 Write +- Edit는 기존 파일 의존 → 순차 처리 +- 에이전트 스폰 대신 단일 세션 다중 도구 호출로 context 오버헤드 최소화 + +## 다음 단계 + +1. **Generator 세션**에서 sut-prober 빌드/실행 → 카탈로그 산출물 생성 +2. **Evaluator 세션**에서 `/evaluate sut-prober` → contract DoD 채점 +3. pass 시 PROGRESS.md → PLAN.md 제거 → P1 항목 착수 diff --git a/docs/sut-catalog/catalog.md b/docs/sut-catalog/catalog.md new file mode 100644 index 0000000..2091a34 --- /dev/null +++ b/docs/sut-catalog/catalog.md @@ -0,0 +1,100 @@ +# SUT Catalog (v0, 정적 분석) + +> 수동 관찰 기반 초기 카탈로그. `sut-prober` PoC 완료 시 기계 생성 파일로 교체된다. +> 마지막 갱신: 2026-04-07 + +## 개요 + +- **이름**: EG-BIM Modeler +- **위치**: `EG-BIM Modeler/` (git 제외) +- **실행파일**: `EG-BIM Modeler.exe` (.NET / WPF) +- **구성**: `.deps.json`, `.runtimeconfig.json`, `.pdb` 동봉 + +## 핵심 어셈블리 + +| 이름 | 역할 | PDB | +|------|------|-----| +| `HmEG.dll` | 자체 3D 엔진 | ✅ (`HmEG.pdb`, `HmEG.xml`) | +| `HmGeometry.dll` / `HmGeometry.V2.dll` | 지오메트리 커널 | ✅ | +| `HmTriangle.dll` | 삼각분할 | ✅ | +| `HmPG.dll` | (추정) 페러미터 그래프/프로파일 | - | +| `HmCommonUI.dll`, `HmCommonBridge.dll` | 공통 UI / 네이티브 브릿지 | - | +| `Editor02.HmEGAppManager.dll` | 앱 생명주기 / plugin 매니저 (MEF 로드 추정) | ✅ | +| `Editor03.PluginInterface.dll` | Plugin 기본 계약 | ✅ | +| `Editor04.CommandControl.dll` | 명령 파이프라인 UI | ✅ | +| `Editor05.CommandCore.dll` | 명령 실행 코어 | ✅ | +| `Editor06.CommandCustom.dll` | 커스텀 명령 | ✅ | +| `Editor07.WidgetPluginInterface.dll` | 위젯 플러그인 계약 | ✅ | +| `EditorCore.dll` | Editor 코어 | ✅ | +| `Editor01.Localization.dll` | 다국어 | ✅ | +| `Editor.AI01.HttpConnector.dll` | AI 연동 | ✅ | + +## 렌더링 + +- **SharpDX** (D3D9/D3D11/DXGI/Direct2D, Mathematics, D3DCompiler) +- 3D 뷰포트는 D3D surface → **UIA 사각지대** +- `assimp.dll` / `AssimpNet.dll` → 메시 임포트 +- `freetype6.dll`, `SharpFont.dll` → 폰트 +- `SharpVectors.*` → SVG 지원 + +## UI 스택 + +- `MahApps.Metro`, `ControlzEx`, `Dragablz` +- `CommunityToolkit.Mvvm` +- `Microsoft.Xaml.Behaviors`, `System.Windows.Interactivity` +- `System.Windows.Controls.WpfPropertyGrid` +- `Microsoft.Web.WebView2.*` (WebView2 패널) + +## 유틸/공용 + +- `Serilog` + `Serilog.Sinks.File`, `log4net` +- `Newtonsoft.Json`, `Google.Protobuf`, `MemoryPack.Core` +- `UnitsNet`, `FluentScheduler`, `System.Reactive`, `Flurl.Http` +- `BaronSoftware.Auth.dll` — 인증/라이선스 추정 (**레코딩 시 민감정보 주의**) + +## Json 설정 (텍스트 golden-file 1차 타깃) + +`EG-BIM Modeler/Json/`: + +- `Settings.json`, `DefaultSettings.json` +- `CategoryCommands.json`, `DefaultCategoryCommands.json` +- `CommandAlias.json`, `DefaultCommandAlias.json` +- `KeyShortCut.json`, `DefaultKeyShortCut.json` +- `MouseSnap.json`, `DefaultMouseSnap.json` +- `StartupCommand.json`, `DefaultStartupCommand.json` +- `Units.json`, `DefaultUnits.json` +- `Materials.json` +- `RecentFiles.json` — **확실히 비결정적** (최근 사용 파일 경로) + +### 비결정성 후보 필드 (정규화 대상) + +- `RecentFiles.json` 전체 +- 모든 `Settings.json` 내부의 경로·창 크기·마지막 실행 시간 +- GUID·타임스탬프 (포맷 확인 필요) + +## 저장 파일 포맷 + +- `lmd.hme` — 자체 모델 포맷 (`.hme`) +- `jversion.egm` — 버전/메타 파일 +- **바이너리 추정** — 별도 포맷 분석 contract 필요 + +## 로컬라이제이션 + +- `ko-KR`, `en-US`, `ja-JP`, `es-ES` 리소스 폴더 +- `Editor01.Localization.dll` 경유 + +## MEF Plugin + +- 위치: `EG-BIM Modeler/Plugins/Eg*Plugin/` +- **총 187개** (2026-04-07 스냅샷) +- 카테고리: 생성(Box, Circle, Arc, Curve, Cone, Cylinder …), Boolean(Union/Intersection/Difference), Array(Linear/Polar/Crv), Align, Check(Border/Disjointed/Duplicated/NonManifold/Self-Intersection/Overlap/ZeroArea/ZeroCurve), Audit, Block(Edit), Chamfer, Clash, Cap, Cut, DeleteFaces, Import/Export 등 +- 전체 목록: [plugins.md](plugins.md) + +## recordingtest 영향 요약 + +1. **1차 검증 타깃**: `Json/` 텍스트 파일 (정규화 쉬움) +2. **2차**: `.hme`/`.egm` 파서 필요 +3. **3D 뷰포트**: 좌표 입력 + engine-bridge +4. **Plugin 카탈로그**: sut-prober 자동 덤프 +5. **PDB 풍부** → engine-bridge 리플렉션 비용 낮음 +6. **인증/라이선스**: 자동화 테스트 환경에서 로그인 상태 유지/마스킹 필요 diff --git a/docs/sut-catalog/plugins.md b/docs/sut-catalog/plugins.md new file mode 100644 index 0000000..3a8fe64 --- /dev/null +++ b/docs/sut-catalog/plugins.md @@ -0,0 +1,199 @@ +# EG-BIM Modeler Plugin List + +- **스냅샷 일시**: 2026-04-07 +- **총 개수**: 187 +- **경로**: `EG-BIM Modeler/Plugins/` + +> 자동 생성 전 수동 덤프. `sut-prober` PoC 완료 시 `plugins.json`으로 교체된다. + +## 전체 목록 (사전순) + +``` +Eg3DFacePlugin +Eg3DMImportExporter +EgAddToGroupPlugin +EgAlignPlugin +EgAlignSelectedVerticesPlugin +EgAlignVerticesPlugin +EgAnglePlugin +EgArcPlugin +EgAreaCentroidPlugin +EgAreaPlugin +EgArrayCrvPlugin +EgArrayLinearPlugin +EgArrayPlugin +EgArrayPolarPlugin +EgAuditPlugin +EgBlockEditPlugin +EgBlockPlugin +EgBooleanDifferencePlugin +EgBooleanIntersectionPlugin +EgBooleanUnionPlugin +EgBoundingBoxPlugin +EgBoxPlugin +EgBuildSectionPlugin +EgCPlanePlugin +EgCapPlugin +EgChamferPlugin +EgChangeColorPlugin +EgCheckBorderEdgePlugin +EgCheckDisjointedMeshPlugin +EgCheckDuplicatedVertexPlugin +EgCheckNonManifoldEdgePlugin +EgCheckSelfIntersectionPlugin +EgCheckSelfOverlapPlugin +EgCheckZeroAreaPlugin +EgCheckZeroCurvePlugin +EgCirclePlugin +EgClashPlugin +EgCloseCrvPlugin +EgClosestPtPlugin +EgConePlugin +EgConvertTextToBlockAttributePlugin +EgCopyPlugin +EgCopyToClipboardPlugin +EgCrvEndPlugin +EgCrvStartPlugin +EgCurvePlugin +EgCurveThroughPtPlugin +EgCutPlugin +EgCylinderPlugin +EgDeleteFacesPlugin +EgDeletePlugin +EgDeleteSubCrvPlugin +EgDeselByUidsPlugin +EgDimAnglePlugin +EgDirPlugin +EgDistancePlugin +EgDividePlugin +EgDomainPlugin +EgDotPlugin +EgDupBorderPlugin +EgDupEdgePlugin +EgDupMeshHoleBoundaryPlugin +EgEditTheGradingElevationPlugin +EgEllipsePlugin +EgEllipsoidPlugin +EgEvaluatePtPlugin +EgExplodeBlockPlugin +EgExplodePlugin +EgExportByPathPlugin +EgExtendPlugin +EgExtractMeshFacePlugin +EgExtractNonManifoldMeshEdgesPlugin +EgExtractPtPlugin +EgExtrudeAlongCrvPlugin +EgExtrudePlugin +EgFillMeshHolePlugin +EgFilletEdgePlugin +EgFilletPlugin +EgFilterByFaceCountPlugin +EgFilterByLayerPlugin +EgFilterByLengthPlugin +EgFilterByModelTypePlugin +EgFilterBySpatialBoxPlugin +EgFilteringWithXYPlanePlugin +EgFindTextPlugin +EgFlipPlugin +EgGroupPlugin +EgHandleCurvePlugin +EgHidePlugin +EgHyperbolaPlugin +EgImportByPathPlugin +EgInsertPlugin +EgInterpCrvPlugin +EgIsolatePlugin +EgJoinPlugin +EgLengthPlugin +EgLinePlugin +EgLineSmoothPlugin +EgLoftPlugin +EgMarginLinePlugin +EgMatchPropertiesPlugin +EgMergeFacesPlugin +EgMeshIntersectPlugin +EgMeshPatchPlugin +EgMeshPolylinePlugin +EgMirrorPlugin +EgModelClearPlugin +EgMovePlugin +EgObjectDescriptionPlugin +EgOffsetMeshPlugin +EgOffsetPlugin +EgOneLayerOffPlugin +EgOneLayerOnPlugin +EgOpenURLPlugin +EgOrientPlugin +EgParabolaPlugin +EgPastePlugin +EgPatchPlugin +EgPipePlugin +EgPlanarDifferencePlugin +EgPlanarIntersectionPlugin +EgPlanarUnionPlugin +EgPlanePlugin +EgPlaneThroughPtPlugin +EgPointPlugin +EgPointsOffPlugin +EgPointsOnPlugin +EgPointsPlugin +EgPolygonCountPlugin +EgPolygonPlugin +EgPolylinePlugin +EgProfileSweepPlugin +EgProjectPlugin +EgProjectionPointPlugin +EgPyramidPlugin +EgRLeaderEditPlugin +EgRebuildPlugin +EgRectanglePlugin +EgReduceMeshPlugin +EgRemoveFromGroupPlugin +EgRemoveSelfIntersectionPlugin +EgRepairCrvTopologyPlugin +EgRevolvePlugin +EgRibbonPlugin +EgRoadSectionPlugin +EgRotate3DPlugin +EgRotatePlugin +EgScale1DPlugin +EgScale2DPlugin +EgScalePlugin +EgScreenShotPlugin +EgSelBoxPlugin +EgSelByUidsPlugin +EgSelColorPlugin +EgSelLayerPlugin +EgSelNamePlugin +EgSelectedLayersOffPlugin +EgSetDisplayModePlugin +EgSetFocusedViewportPlugin +EgSetGroupNamePlugin +EgSetObjectNamePlugin +EgSetViewmodePlugin +EgShearPlugin +EgShowPlugin +EgSlabPlugin +EgSlopePlugin +EgSnipCrvPlugin +EgSpherePlugin +EgSplitDisjointMeshPlugin +EgSplitPlugin +EgSrfPtPlugin +EgSubCrvPlugin +EgSurfaceQuadRemeshPlugin +EgSweep1Plugin +EgSweep2Plugin +EgTextPlugin +EgTorusPlugin +EgTrimPlugin +EgTruncatedPyramidPlugin +EgTubePlugin +EgUnGroupPlugin +EgUngroupAllPlugin +EgUnifyNormalPlugin +EgVerticalScalePlugin +EgVolumeCentroidPlugin +EgZoomPlugin +HMEGImportExport +``` diff --git a/recordingtest.sln b/recordingtest.sln new file mode 100644 index 0000000..70879aa --- /dev/null +++ b/recordingtest.sln @@ -0,0 +1,16 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.SutProber", "src\Recordingtest.SutProber\Recordingtest.SutProber.csproj", "{1A0B2C3D-0001-0000-0000-000000000001}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1A0B2C3D-0001-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A0B2C3D-0001-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A0B2C3D-0001-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A0B2C3D-0001-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Recordingtest.SutProber/AssemblyScanner.cs b/src/Recordingtest.SutProber/AssemblyScanner.cs new file mode 100644 index 0000000..6a68d10 --- /dev/null +++ b/src/Recordingtest.SutProber/AssemblyScanner.cs @@ -0,0 +1,25 @@ +namespace Recordingtest.SutProber; + +public sealed record AssemblyEntry(string Name, long SizeBytes, bool HasPdb); + +public static class AssemblyScanner +{ + private static readonly string[] Prefixes = { "HmEG", "HmGeometry", "HmTriangle", "HmPG", "HmCommon", "Editor", "EditorCore" }; + + public static List Scan(string sutRoot) + { + var entries = new List(); + if (!Directory.Exists(sutRoot)) return entries; + + foreach (var file in Directory.EnumerateFiles(sutRoot, "*.dll", SearchOption.TopDirectoryOnly) + .OrderBy(f => f, StringComparer.Ordinal)) + { + var name = Path.GetFileName(file); + if (!Prefixes.Any(p => name.StartsWith(p, StringComparison.Ordinal))) continue; + + var pdb = Path.ChangeExtension(file, ".pdb"); + entries.Add(new AssemblyEntry(name, new FileInfo(file).Length, File.Exists(pdb))); + } + return entries; + } +} diff --git a/src/Recordingtest.SutProber/JsonConfigScanner.cs b/src/Recordingtest.SutProber/JsonConfigScanner.cs new file mode 100644 index 0000000..411b370 --- /dev/null +++ b/src/Recordingtest.SutProber/JsonConfigScanner.cs @@ -0,0 +1,51 @@ +using System.Text.Json; + +namespace Recordingtest.SutProber; + +public sealed record JsonConfigEntry( + string Name, + IReadOnlyList TopLevelKeys, + IReadOnlyList SuspectedNondeterministicFields); + +public static class JsonConfigScanner +{ + private static readonly string[] SuspectPatterns = + { "time", "date", "path", "recent", "guid", "id", "machine", "user" }; + + public static List Scan(string sutRoot) + { + var jsonRoot = Path.Combine(sutRoot, "Json"); + if (!Directory.Exists(jsonRoot)) return new(); + + var entries = new List(); + foreach (var file in Directory.EnumerateFiles(jsonRoot, "*.json").OrderBy(f => f, StringComparer.Ordinal)) + { + var name = Path.GetFileName(file); + var keys = new List(); + var suspects = new List(); + try + { + using var doc = JsonDocument.Parse(File.ReadAllText(file)); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var p in doc.RootElement.EnumerateObject()) + { + keys.Add(p.Name); + var lower = p.Name.ToLowerInvariant(); + if (SuspectPatterns.Any(sp => lower.Contains(sp))) + suspects.Add(p.Name); + } + } + } + catch (JsonException) + { + keys.Add(""); + } + + keys.Sort(StringComparer.Ordinal); + suspects.Sort(StringComparer.Ordinal); + entries.Add(new JsonConfigEntry(name, keys, suspects)); + } + return entries; + } +} diff --git a/src/Recordingtest.SutProber/PluginScanner.cs b/src/Recordingtest.SutProber/PluginScanner.cs new file mode 100644 index 0000000..d41fbd0 --- /dev/null +++ b/src/Recordingtest.SutProber/PluginScanner.cs @@ -0,0 +1,32 @@ +namespace Recordingtest.SutProber; + +public sealed record PluginEntry(string Name, string Path, IReadOnlyList Dlls, long SizeBytes); + +public static class PluginScanner +{ + public static List Scan(string sutRoot) + { + var pluginRoot = System.IO.Path.Combine(sutRoot, "Plugins"); + if (!Directory.Exists(pluginRoot)) return new(); + + var entries = new List(); + foreach (var dir in Directory.EnumerateDirectories(pluginRoot).OrderBy(d => d, StringComparer.Ordinal)) + { + var dlls = Directory.EnumerateFiles(dir, "*.dll", SearchOption.TopDirectoryOnly) + .Select(System.IO.Path.GetFileName) + .Where(n => n is not null) + .Select(n => n!) + .OrderBy(n => n, StringComparer.Ordinal) + .ToList(); + + long size = 0; + foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) + size += new FileInfo(f).Length; + + var name = System.IO.Path.GetFileName(dir); + var relPath = System.IO.Path.GetRelativePath(".", dir).Replace('\\', '/'); + entries.Add(new PluginEntry(name, relPath, dlls, size)); + } + return entries; + } +} diff --git a/src/Recordingtest.SutProber/Program.cs b/src/Recordingtest.SutProber/Program.cs new file mode 100644 index 0000000..268a41e --- /dev/null +++ b/src/Recordingtest.SutProber/Program.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using Recordingtest.SutProber; + +// Static-only probe of the EG-BIM Modeler SUT folder. NEVER launches the SUT. +// See docs/contracts/sut-prober.md for the sprint contract. + +string sutPath = "EG-BIM Modeler"; +string outDir = Path.Combine("docs", "sut-catalog"); + +for (int i = 0; i < args.Length; i++) +{ + if (args[i] == "--sut" && i + 1 < args.Length) sutPath = args[++i]; + else if (args[i] == "--out" && i + 1 < args.Length) outDir = args[++i]; +} + +if (!Directory.Exists(sutPath)) +{ + Console.Error.WriteLine($"SUT path not found: {sutPath}"); + return 2; +} + +Directory.CreateDirectory(outDir); + +var plugins = PluginScanner.Scan(sutPath); +var jsonConfigs = JsonConfigScanner.Scan(sutPath); +var assemblies = AssemblyScanner.Scan(sutPath); + +var opts = new JsonSerializerOptions +{ + WriteIndented = true, + // deterministic property order follows POCO definition +}; + +File.WriteAllText(Path.Combine(outDir, "plugins.json"), + JsonSerializer.Serialize(plugins, opts)); +File.WriteAllText(Path.Combine(outDir, "json-configs.json"), + JsonSerializer.Serialize(jsonConfigs, opts)); +File.WriteAllText(Path.Combine(outDir, "assemblies.json"), + JsonSerializer.Serialize(assemblies, opts)); + +Console.WriteLine($"Wrote catalog to {outDir}/ — plugins: {plugins.Count}, json: {jsonConfigs.Count}, assemblies: {assemblies.Count}"); +return 0; diff --git a/src/Recordingtest.SutProber/Recordingtest.SutProber.csproj b/src/Recordingtest.SutProber/Recordingtest.SutProber.csproj new file mode 100644 index 0000000..6f594d2 --- /dev/null +++ b/src/Recordingtest.SutProber/Recordingtest.SutProber.csproj @@ -0,0 +1,10 @@ + + + Exe + Recordingtest.SutProber + Recordingtest.SutProber + + + + +