Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/chad/done-guard-window/done-guard-window-artifact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# done-guard-window — Artifact

*최종 결과물과 학습 내용을 기록한다.*

## 결과

**조사·설계까지 완료 · 구현 미착수.** (2026-08-25)

| 항목 | 상태 |
|---|---|
| 오탐 실사례 확보 | ✅ `boundary-perf-invariant` 종결 시 가드 2건 |
| 원인 확정 | ✅ `switchedAt`이 "마지막 활성화"라 재활성화가 판정 창을 초기화 |
| 영향 범위 특정 | ✅ `collectDoneIssues()` 의존 가드 3종 (`:442`·`:474`·`:486`) |
| 설계 긴장 확인 | ✅ `tests/done-guard.test.mjs:461`이 "옛 마커 차단"을 의도로 고정 |
| 창 기준 결정 | ⏳ (A) `firstActivatedAt` 유력, 미확정 |
| 구현·테스트 | ⏳ 미착수 |

한 줄 요약: `done` 가드가 증거를 찾는 창이 `[switchedAt, now]`인데, `switchedAt`은
**마지막 활성화 시각**이라 재활성화하면 이미 만족된 증거가 창 밖으로 밀려난다.
있어야 할 것은 `[task 작업 시작, now]`다.

**주의:** 이 task의 핵심 난점은 "오탐을 없애는 것"이 아니라 **"오탐만 없애는 것"** 이다.
창을 넓히면 오탐은 사라지지만 가드가 막던 망각도 함께 통과한다 — spec 요구사항 2가 금지한다.

## 조사 기록 (2026-08-25)

### 어떻게 드러났나

`boundary-perf-invariant`(plan 14/14, PR #44 머지 완료)를 종결하려 하자 가드 2건이 걸렸다:

```
cause: spec이 `review: required`인데 이 task 기간의 리뷰 마커가 artifact에 없음
cause: task 활성화 이후 커밋이 0개임
```

둘 다 오탐이었다. `done`은 활성 task만 대상이므로 끝난 task를 종결하려면 **재활성화**해야 하는데,
그것이 `switchedAt`을 `04:25:13Z` → `09:19:47Z`로 갱신했다.

| 가드 | 실제 증거 | 새 창 기준 판정 |
|---|---|---|
| 리뷰 마커 | `at=05:35:00Z` (codex 리뷰) | 창 밖 → "없음" |
| 커밋 | 6건 `9414faf`…`f704672` (PR #44로 머지) | 창 밖 → "0개" |

`--force`로 우회했고, **그 우회 자체가 이 task를 만든 이유다.**

### 왜 구조적인가

`runTask`는 생성(`:204`)과 **재활성화(`:231`) 모두** `switchedAt`을 현재 시각으로 덮어쓴다.
따라서:
- 끝난 task를 닫을 때마다 **반드시** 이 오탐이 난다 — 종결에는 재활성화가 선행되므로.
- 종결만의 문제도 아니다. **task를 전환했다가 되돌아오는 것만으로도** 창이 초기화되어
그 전 작업 증거가 통째로 사라진다.

즉 "가끔 나는 버그"가 아니라 **정상 경로에서 항상 재현되는 구조**다.
이대로면 `--force`가 종결의 표준 절차가 되고, 가드는 이름만 남는다.

### 설계 긴장 (이 task의 진짜 어려움)

기존 테스트가 반대 방향을 못박아 두고 있다 —
`tests/done-guard.test.mjs:461` *"이전 task 기간의 마커(at < switchedAt)는 무효 → 차단"*.
**옛 리뷰 마커를 재사용해 통과하는 것을 막는 것이 의도**다.

그러므로 수정은 "옛 마커를 허용"하는 방향이면 안 된다. 되살려야 하는 것은
**같은 task의 이전 활성 구간**뿐이고, 다른 task 기간의 마커는 계속 막아야 한다.
이 구분이 되지 않으면 오탐은 사라져도 가드는 무력해진다.

### 후보 요약

(A) `firstActivatedAt` 신규 필드 — **유력**. 의미가 정확하고 재활성화가 건드리지 않는다.
(B) `meta.created` 재사용 — **부적합**. `slice(0,10)`이라 날짜만이라 창이 과도하게 넓다.
(C) 활성 구간 누적 — 가장 정확하나 상태·복잡도 증가.
(D) task 디렉터리 첫 커밋에서 유도 — 새 상태 불필요, (A)와 조합 검토 가치.
(E) 종결 시에만 완화 — 증상만 가림, 전환 케이스 미해결.

상세·평가는 spec의 "후보" 표가 정본이다.

## Reviews

## Learnings

- **오탐은 가드를 죽인다.** 이 건은 "가드가 틀렸다"보다 **"가드를 우회하는 것이 정상 절차가 된다"**
가 진짜 위험이다. 실제로 이번에 `--force`를 썼고, 같은 구조가 남아 있는 한 다음에도 쓰게 된다.
오탐률은 정확도 문제가 아니라 **신뢰 문제**다.
- **"마지막 X 시각"을 "X 구간의 시작"으로 쓰면 안 된다.** 두 개념이 대부분의 경우 우연히
일치해서 오래 버그가 드러나지 않는다 — 재활성화처럼 갈라지는 순간에만 터진다.
- **가드를 고칠 때의 판정 기준은 "통과하는가"가 아니라 "여전히 막는가"** 다.
이 task의 성공 조건에 기존 차단 테스트(`:461`) 통과를 명시적으로 넣은 이유다.
35 changes: 35 additions & 0 deletions docs/chad/done-guard-window/done-guard-window-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# done-guard-window — Context Card
<!-- working set only; UTF-8 <= 6 KiB, nonblank lines <= 100 -->

## Now
- Goal: `done` 가드의 판정 창을 "마지막 활성화" → "task 작업 구간"으로 바꿔 오탐 제거.
**단 가드가 막던 망각은 계속 잡아야 한다.**
- Current atomic step: **[결정] 창 기준 선택** — (A) `firstActivatedAt` 유력.
결정 후 **재현 테스트 먼저**, 그다음 구현.
- Stop / human-decision condition: 없음(기술 결정). 다만 착수 지시를 받고 시작한다 —
현재는 조사·설계까지만 된 상태다.

## Constraints and settled decisions
- 원인 확정: `switchedAt`은 **마지막 활성화 시각**이지 작업 구간이 아니다.
`runTask`가 생성(`:204`)·재활성화(`:231`) 모두 현재 시각으로 덮어쓴다.
- 의존 가드 3종: 리뷰 마커(`:442`) · 커밋 0개(`:474`) · 테스트 미작성(`:486`).
- **문제는 종결 케이스보다 넓다** — task 전환 후 복귀만으로도 창이 초기화된다.
- **가드를 넓혀서 통과시키면 안 된다.** `tests/done-guard.test.mjs:461`이
"옛 마커 재사용 차단"을 의도로 고정해 뒀다 — 이 테스트가 계속 통과해야 한다.
- `meta.created`는 **날짜만**(`slice(0,10)`)이라 창 시작으로 부적합(과도하게 넓어짐).
- 하위 호환: 새 필드가 없으면 **degrade**(차단 아님). `switchedAt` 부재 시 기존 처리(`:444`)와 동일 원칙.

## JIT retrieval map
- Narrow globs: `src/commands/task.mjs`, `tests/done-guard.test.mjs`
- Identifiers: `collectDoneIssues`, `switchedAt`, `writeActive`, `parseReviewMarkers`, `readTaskMeta`
- Read next: `collectDoneIssues()` `:402-500` → `runTask()` `:200-235`
- Verification command: `node --test tests/done-guard.test.mjs` (27 test)

## Failure capsules (max 3 unresolved)
- (none unresolved — 원인 확정, 남은 것은 설계 선택과 구현이다)

## Resume checklist
- 실사례는 `docs/chad/boundary-perf-invariant/…-artifact.md` `## 종결 기록`에 있다 —
재현이 필요하면 거기 타임스탬프(활성화 04:25:13Z / 마커 05:35:00Z / 재활성화 09:19:47Z)를 쓴다.
- **테스트를 먼저 쓴다.** 가드 자신을 고치는 task라 "고쳤다"의 근거가 테스트여야 한다.
- 판정 기준은 "오탐이 사라졌는가"가 아니라 **"오탐은 사라지고 `:461`은 계속 막는가"** 다.
3 changes: 3 additions & 0 deletions docs/chad/done-guard-window/done-guard-window-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# done-guard-window — Handoff

(세션 종료 시 post-commit hook이 자동 갱신합니다)
7 changes: 7 additions & 0 deletions docs/chad/done-guard-window/done-guard-window-meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"user": "chad",
"task": "done-guard-window",
"created": "2026-08-25",
"status": "open",
"closedAt": null
}
28 changes: 28 additions & 0 deletions docs/chad/done-guard-window/done-guard-window-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# done-guard-window — Plan

## 목표
`done` 가드의 판정 창을 "마지막 활성화"가 아니라 "task 작업 구간"으로 바꿔 오탐을 없애되,
가드가 막던 **망각**은 계속 잡게 한다.

## 단계
- [x] 오탐 실사례 확보 — `boundary-perf-invariant` 종결 시 가드 2건, 원인은 재활성화
- [x] 영향 범위 특정 — `collectDoneIssues()`의 `switchedAt` 의존 가드 3종(`:442`·`:474`·`:486`)
- [x] 문제가 종결 케이스보다 넓음을 확인 — **task 전환 후 복귀만으로도** 창이 초기화된다
- [x] 설계 긴장 확인 — `tests/done-guard.test.mjs:461`이 "옛 마커 차단"을 의도로 고정
- [ ] **[결정] 창 기준 선택** — (A) `firstActivatedAt` 유력 · (B) `meta.created`는 날짜만이라 부적합
· (D) task 디렉터리 첫 커밋 조합 검토 · 마이그레이션/하위 호환 설계 포함
- [ ] **재현 테스트 먼저 작성** — 재활성화 후 원래 창의 마커·커밋이 인정되는지 (지금은 실패해야 함)
- [ ] 구현 — 창 기준 교체 + 재활성화가 창 시작을 덮어쓰지 않게
- [ ] **회귀 확인** — `:461` 옛 마커 재사용 차단 테스트가 **계속 통과**하는지가 핵심 판정
- [ ] 하위 호환 — 새 필드 없는 기존 task는 degrade(차단하지 않음) 확인
- [ ] spec Ambiguity 게이트 닫고 artifact 기록
- [ ] 외부 리뷰(`/harness-review`) 후 `## Reviews` 기록 (spec이 `review: required`)

## Ontology 변경 로그
- **판정 창(evidence window)** 개념을 `switchedAt`과 분리해 신설. 둘을 같은 것으로 다룬 것이
버그의 원인이며, 이 분리가 수정의 축이다.
- **가드 약화** 를 명시적 실패 양식으로 등재 — 오탐을 없애려다 진짜 누락까지 통과시키는 것.

## 참고
- 이 task는 **하네스 자신의 가드**를 고친다. 스스로를 검증하는 코드이므로 테스트 우선으로 간다.
- 우회 이력: 2026-08-25 `done --force` 1회 (사유는 boundary-perf-invariant artifact에 기록).
115 changes: 115 additions & 0 deletions docs/chad/done-guard-window/done-guard-window-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# done-guard-window — Spec

## 목적 / 요구사항

`harness-team done`의 증거 기반 가드가 **구조적으로 오탐을 낸다.** 판정 창의 기준이
`active.json`의 `switchedAt`인데, 이 값은 "**마지막 활성화 시각**"이지 "**이 task의 작업 구간**"이
아니다. task를 다시 활성화하면 창이 초기화되어, 이미 만족된 증거가 사라진 것처럼 보인다.

**2026-08-25 실제 발생.** `boundary-perf-invariant`(plan 14/14, PR #44 머지 완료)를 종결하려는데
가드 2건이 걸렸다:

```
cause: spec이 `review: required`인데 이 task 기간의 리뷰 마커가 artifact에 없음
cause: task 활성화 이후 커밋이 0개임
```

둘 다 **오탐**이었다. `done`은 활성 task만 대상이라 끝난 task를 종결하려면 재활성화해야 하는데,
그 재활성화가 `switchedAt`을 `04:25:13Z` → `09:19:47Z`로 밀어 버렸다. 실제 증거는 원래 창 안에 있었다:

| 가드 | 실제 증거 | 새 창(09:19) 기준 |
|---|---|---|
| 리뷰 마커 | `at=05:35:00Z` (codex 리뷰 기록) | 창 **밖** → 없다고 판정 |
| 커밋 | 6건 (`9414faf`…`f704672`, 전부 PR #44로 머지) | 창 **밖** → 0개로 판정 |

결국 `--force`로 우회했다. **이것이 이 task가 막으려는 결과다** —
가드를 우회하는 것이 정상 절차가 되면 가드는 더 이상 아무것도 막지 못한다.

요구사항:
1. 재활성화가 증거를 무효화하지 않게 한다.
2. **가드의 원래 힘을 약화시키지 않는다.** 창을 넓히는 것만으로 "green"을 사면 안 된다 —
이 가드가 막으려던 "망각"을 계속 잡아야 한다.
3. 기존 의도(옛 리뷰 마커 재사용 방지)를 보존한다.

## 설계 / 접근

### 영향 범위 — `switchedAt`에 의존하는 가드 3종

`src/commands/task.mjs`의 `collectDoneIssues()`:

| 위치 | 가드 | 사용 방식 |
|---|---|---|
| `:442-446` | 리뷰 마커 신선도 | `m.at >= Date.parse(switchedAt)` |
| `:474-480` | 활성화 이후 커밋 0개 | `git log --since=<switchedAt>` |
| `:486-497` | 테스트 미작성 | `git log --since=<switchedAt> --name-only` |

`switchedAt`은 `runTask`에서 **생성 시(`:204`)와 재활성화 시(`:231`) 모두 현재 시각으로 덮어쓴다.**

### 문제는 종결보다 넓다

"끝난 task를 닫을 때"만의 문제가 아니다. **task를 전환했다가 되돌아오는 것만으로도** 창이
초기화되어 이전 작업 증거가 통째로 사라진다. 여러 task를 오가는 작업 방식에서는
항상 발생한다.

### 설계 긴장 — 그냥 넓히면 안 된다

기존 테스트가 의도를 명시적으로 고정해 두고 있다:
`tests/done-guard.test.mjs:461` — *"이전 task 기간의 마커(at < switchedAt)는 무효 → 차단"*.
즉 **옛 리뷰 마커를 재사용해 통과하는 것을 막는 것이 설계 의도**다.
수정은 이 의도를 유지하면서 "**같은 task의 이전 활성 구간**"만 되살려야 한다.
"옛 마커 전부 허용"으로 가면 가드가 무력해진다 — 요구사항 2 위반.

### 후보

| 후보 | 내용 | 평가 |
|---|---|---|
| **(A) `firstActivatedAt` 도입** | 최초 활성화 시각을 meta에 기록하고 창의 시작으로 쓴다. 재활성화는 이 값을 **건드리지 않는다** | **유력.** 의미가 정확("task 작업 구간의 시작"). 새 필드 1개 |
| (B) `meta.created` 사용 | 이미 있는 필드 | ⚠️ **날짜만**(`slice(0,10)`)이라 그날 00:00로 뭉개진다. 오래된 task는 창이 과도하게 넓어져 가드가 약해짐 |
| (C) 활성 구간 누적 | `[{from,to}...]`를 기록해 합집합으로 판정 | 가장 정확하지만 상태·복잡도가 크게 증가 |
| (D) task 디렉터리 이력에서 유도 | `docs/<user>/<task>/`를 처음 건드린 커밋을 창 시작으로 | 새 상태 불필요. 단 커밋 전 task는 창이 없음 |
| (E) 종결 시에만 창 완화 | `done` 경로에서만 넓게 본다 | 증상만 가림 · 전환 케이스는 그대로 |

**(A)가 유력**하나 확정하지 않았다 — 마이그레이션(기존 `active.json`·`meta.json`에 필드 없음)과
하위 호환 처리를 함께 설계해야 하고, (D)와의 조합도 검토 가치가 있다.

### 하위 호환

`switchedAt`이 없는 구 `active.json`은 이미 "시각 비교 포기, 마커 존재만 확인"으로
degrade한다(`:444` 주석). 새 필드도 **없으면 degrade**하는 같은 원칙을 따라야 한다 —
없다고 차단하면 기존 task가 전부 막힌다.

## Ontology

- **`switchedAt`**: 마지막 활성화 시각. **task 작업 구간의 시작이 아니다** — 이 혼동이 버그의 원인.
- **판정 창(evidence window)**: 가드가 "이 task 기간의 증거"를 찾는 시간 범위.
현재 `[switchedAt, now]`이며, 있어야 할 것은 `[task 작업 시작, now]`.
- **오탐(false positive)**: 증거가 실제로 존재하는데 창 밖이라 없다고 판정되는 것.
**가드 무력화의 전형적 경로** — 오탐이 반복되면 `--force`가 습관이 된다.
- **가드 약화**: 창을 넓혀 오탐을 없앴지만 진짜 누락도 함께 통과시키는 것. 요구사항 2가 금지한다.

## Ambiguity 자가진단

- [x] **Goal 명확도** (40%) — "재활성화가 증거를 무효화하지 않게 하되 가드 힘은 유지한다".
- [x] **Constraint 명확도** (30%) — 기존 의도(옛 마커 재사용 차단) 보존 · 하위 호환 degrade ·
가드 완화 금지. `tests/done-guard.test.mjs`(27 test)가 회귀 그물이다.
- [x] **Success 기준** (30%) — 재활성화 후에도 원래 창의 마커·커밋이 인정되고,
**옛 마커 재사용 차단 테스트는 계속 통과**한다. 재현 테스트를 먼저 추가한다.
- [x] **Context 명확도** — `src/commands/task.mjs`(`collectDoneIssues` · `runTask`) ·
`tests/done-guard.test.mjs`. 영향 범위가 좁고 특정됐다.
- [ ] **Ambiguity ≤ 0.2** — **가중합 1.0이나 구현 설계(A~D 중 선택 + 마이그레이션)가
미확정**이라 게이트를 열어 둔다. 정책이 아니라 **기술 결정**이므로 구현 단계에서
후보를 좁혀 닫는다. 착수 지시가 오면 (A) 기준으로 진행하되 (D) 조합을 검토한다.

## Done evidence

```json
{ "version": 1, "review": "required", "tests": "required" }
```

<!-- 가드 자신을 고치는 task다. 테스트 없이 닫으면 같은 종류의 신뢰 문제가 반복되므로
tests·review 를 모두 required 로 선언한다. -->

## 참고
- 발생 기록: `docs/chad/boundary-perf-invariant/boundary-perf-invariant-artifact.md` `## 종결 기록`
- 관련 구현: `src/commands/task.mjs` `collectDoneIssues()` · `runTask()`
- 회귀 그물: `tests/done-guard.test.mjs` (특히 `:461` 옛 마커 차단 테스트)
Loading