Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
01f5345
docs(#251): PR3c 착수 전 라운드 — 앵커 정렬 키 확정·판정식 오탐 폐기·PR3c/PR3c′ 재분할
pdw96 Aug 12, 2026
e9350e8
docs(#251): 체크포인트 1R 반영 — refRevision off-by-one 정정·앵커 계층 판정·T73~T82 신설
pdw96 Aug 12, 2026
2fc187e
docs(#251): 체크포인트 2R 반영 — 조건부 면제 아키텍처·판정 계층 개정·T83~T94
pdw96 Aug 12, 2026
bb17866
docs(#251): 체크포인트 3R Approved — 인터리브 (A) 확정·verdict 개명·두단계 앞섬 봉쇄
pdw96 Aug 12, 2026
b572a6c
feat(#251-PR3c): 결과 ref 문법 · 전이 불변식 계층 (T75·T82)
pdw96 Aug 12, 2026
45c6445
chore(brain): result-ref.ts 신설 반영 (#251-PR3c)
pdw96 Aug 12, 2026
cad8d1d
feat(#251-PR3c): 복구 판정 계층 · 앵커 게이트 · §3 행 T73~T94 (T13b′)
pdw96 Aug 12, 2026
670af27
docs(#251-PR3c): 이월 정정 70·85 문면 반영 — T20 귀속 분리 · authority 주석 갱신
pdw96 Aug 12, 2026
7f39144
fix(#251-PR3c): Codex PR#289 1R P1×5 반영 — 전부 실재 · 전부 수용
pdw96 Aug 12, 2026
4be2938
fix(#251-PR3c): CodeRabbit 1R 반영 — 완결 귀속 저널 blocker · 제어문자 오염 재발 차단
pdw96 Aug 12, 2026
86e75f6
docs(#251-PR3c): 정정 214 자기반박 — 「verify 는 NUL 을 안 본다」는 거짓이었다
pdw96 Aug 12, 2026
9c1382a
fix(#251-PR3c): Codex 3R P1×3 반영 — 결속 판정 · lifecycle 시작 게이트 · 완결 귀속 도입
pdw96 Aug 12, 2026
d231532
fix(#251-PR3c): Codex 4R P1×5 반영 — 저널 ref 이름 검증 · draft 결속 · 봉인 확장
pdw96 Aug 12, 2026
bcd4696
fix(#251-PR3c): Codex 5R P1×4 반영 — stage 별 ref 산술 · ref 요구 · 완결 전제 · …
pdw96 Aug 12, 2026
7676885
fix(#251-PR3c): Codex 6R P1×6 반영 — 검사의 정의역을 맞춘다
pdw96 Aug 12, 2026
099874b
test(#251-PR3c): CodeRabbit 지적 반영 — 가려진 두 테스트의 반증력 복원
pdw96 Aug 12, 2026
3c9ec51
fix(#251-PR3c): 로컬 적대 리뷰 반영 — 열린 창 결속 재설계(설계급 P1) · 반증력 14건 복원
pdw96 Aug 12, 2026
aeca824
docs(#251-PR3c): CodeRabbit 2R 반영 — ref revision 산술 드리프트 전수 정정 · 자가 회…
pdw96 Aug 13, 2026
ec1dbdf
fix(#251-PR3c): Codex 7R P1×5 반영 — 면제·존재 전제·조기 반환이 좁힌 정의역을 되돌린다
pdw96 Aug 13, 2026
1bac922
fix(#251-PR3c): Codex 8R P1×3 반영 — 결속의 형태를 맞춘다(존재 술어 · 다중 관측 · 무제약 arm)
pdw96 Aug 13, 2026
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
14 changes: 9 additions & 5 deletions brain.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Fleet — 코드베이스 브레인 (자동 생성)

> `npm run brain` 로 `src/` 에서 자동 추출한 구조 지도다. **코드를 탐색하기 전에 이 파일을 먼저 읽어** 토큰을 아껴라.
> 100 files · 242 import wires · 47 IPC channels · 생성 2026-08-10T16:31 UTC
> 102 files · 246 import wires · 47 IPC channels · 생성 2026-08-13T10:46 UTC
> 표기: `파일 — 역할 · →의존 · ←피의존`. id 는 `main/core/` 생략(예: `session/manager`).

## 레이어 (위 → 아래로 흐름)
Expand Down Expand Up @@ -108,19 +108,23 @@
- **workbench/locks**
- →의존: workbench/authority, workbench/coord-area, workbench/ulid · ←피의존: workbench/__testing__/lock-backend-fake, workbench/active-instance, workbench/authority, workbench/journal, workbench/lock-backend-uds, workbench/lock-order · 467줄
- **workbench/authority**
- →의존: shared/types, workbench/durable-fs, workbench/locks · ←피의존: workbench/journal, workbench/locks · 1664줄
- →의존: shared/types, workbench/durable-fs, workbench/locks · ←피의존: workbench/journal, workbench/locks, workbench/recovery · 1901줄
- **workbench/journal**
- →의존: workbench/authority, workbench/durable-fs, workbench/locks, workbench/ulid · ←피의존: · 1014줄
- →의존: workbench/authority, workbench/durable-fs, workbench/locks, workbench/ulid · ←피의존: workbench/recovery · 1038줄
- **workbench/active-instance**
- →의존: workbench/instance-marker, workbench/locks, workspace/path-guard · ←피의존: — · 397줄
- **workbench/coord-area**
- →의존: workspace/git, workspace/path-guard · ←피의존: workbench/locks · 372줄
- **workbench/durable-fs**
- →의존: — · ←피의존: workbench/__testing__/durable-fs-fake, workbench/authority, workbench/journal · 214줄
- **workbench/recovery**
- →의존: workbench/authority, workbench/journal, workbench/result-ref · ←피의존: — · 834줄
- **workbench/ulid**
- →의존: — · ←피의존: workbench/journal, workbench/locks, workbench/result-ref · 86줄
- **workbench/instance-marker**
- →의존: — · ←피의존: workbench/active-instance, workbench/instance-marker-proc · 116줄
- **workbench/ulid**
- →의존: · ←피의존: workbench/journal, workbench/locks · 86줄
- **workbench/result-ref**
- →의존: workbench/ulid · ←피의존: workbench/recovery · 125줄
- **workbench/__testing__/durable-fs-fake**
- →의존: workbench/durable-fs · ←피의존: — · 277줄
- **workbench/__testing__/lock-backend-fake**
Expand Down
366 changes: 361 additions & 5 deletions docs/superpowers/plans/2026-07-23-issue251-workbench-core.md

Large diffs are not rendered by default.

236 changes: 212 additions & 24 deletions docs/superpowers/specs/2026-07-23-ade-workbench-spec.md

Large diffs are not rendered by default.

143 changes: 139 additions & 4 deletions src/main/core/workbench/authority-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,12 +639,27 @@ describe('§3-T13 revision-CAS', () => {
it('ⓑ 첫 draft 가 세운 옵셔널이 두 번째 커밋에 살아남지 않는다(LWW 병합이면 RED)', async () => {
const { store, lease, benchId, fs, path } = await setup()

// 4필드를 전부 세우려면 `composed` 가 필요한데(`prepared` 는 `resultOid` 를 실을 수 없다 ·
// 불변식 ③c), **최초 레코드는 `prepared` 로만 시작할 수 있다**(Codex PR#289 P1). 그래서 두 단계로
// 올라간 뒤 4필드가 전부 선 상태를 만든다 — 지름길이 막힌 것이지 시나리오가 바뀐 것은 아니다.
await store.withAuthority(lease, async (tx) => {
const read = tx.readFresh()
if (read.kind !== 'absent') throw new Error('absent 예상')
return tx.compareAndSwap(
read.read,
// `composed` 를 쓴다 — `prepared` 는 `resultOid` 를 실을 수 없다(불변식 ③c · Codex 2R P1-E).
draft(benchId, {
currentIntegrationTxnId: 'T1',
currentIntegrationStage: 'prepared',
currentIntegrationTxnGeneration: 1,
}),
)
})

await store.withAuthority(lease, async (tx) => {
const read = tx.readFresh()
if (read.kind !== 'found') throw new Error('found 예상')
return tx.compareAndSwap(
read.read,
draft(benchId, {
currentIntegrationTxnId: 'T1',
currentIntegrationStage: 'composed',
Expand Down Expand Up @@ -1782,11 +1797,48 @@ describe('store 옵션은 생성 시점 스냅샷이다(Codex 2R P1-C)', () => {
* 그래서 그 규칙은 **WAL 이 첫 단계에 진입조차 못 하게** 만들었다.
*/
describe('통합 WAL 단계별 resultOid 규칙(Codex 2R P1-E)', () => {
const STAGE_PATH = ['prepared', 'composed', 'published', 'finalized'] as const

/**
* 목표 stage 까지 **WAL 순서대로 올라간 뒤** 마지막 draft 를 커밋한다.
*
* ⚠ 이전 판은 **첫 CAS 로 곧바로 목표 stage 를 만들었다**(픽스처 지름길). Codex PR#289 P1 이후
* 「최초 레코드는 `prepared` 로만 통합을 시작한다」가 전이 불변식으로 서면서 그 지름길은 표현
* 불가능해졌고, **지름길이 성립했다는 사실 자체가 결함**이었다 — 첫 CAS 가 `composed` 로 태어나면
* WAL 의 `prepared` 단계와 그 크래시 안전 순서를 통째로 건너뛴다. 이 describe 가 검증하는 것은
* **resultOid 규칙**이지 진입 경로가 아니므로, 경로만 합법으로 바꿔 의도를 보존한다.
*/
const casWith = async (over: Partial<BenchAuthorityDraft>): Promise<CasResult> => {
const fx = await setup()
const target = over.currentIntegrationStage
const climb =
target === undefined || target === 'prepared'
? []
: // ⚠ `abandoned` 도 **prepared 를 먼저 커밋한 뒤** 제출한다(CodeRabbit PR#289): 첫 CAS 로
// 내면 「최초는 prepared 로만」이 먼저 답해서, 이 행이 겨냥한 「권위는 abandoned 를 갖지
// 않는다」(정정 177)를 **한 번도 검증하지 못한다**(가림).
target === 'abandoned'
? (['prepared'] as const)
: STAGE_PATH.slice(0, STAGE_PATH.indexOf(target))
for (const stage of climb) {
const step = await fx.store.withAuthority(fx.lease, async (tx) => {
const read = tx.readFresh()
if (read.kind !== 'absent' && read.kind !== 'found') throw new Error(read.kind)
return tx.compareAndSwap(
read.read,
draft(fx.benchId, {
currentIntegrationTxnId: 'T1',
currentIntegrationStage: stage,
currentIntegrationTxnGeneration: 1,
...(stage === 'prepared' ? {} : { currentIntegrationResultOid: 'oid1' }),
}),
)
})
if (step.kind !== 'committed') return step
}
return fx.store.withAuthority(fx.lease, async (tx) => {
const read = tx.readFresh()
if (read.kind !== 'absent') throw new Error('absent 예상')
if (read.kind !== 'absent' && read.kind !== 'found') throw new Error(read.kind)
return tx.compareAndSwap(read.read, draft(fx.benchId, over))
})
}
Expand Down Expand Up @@ -1840,12 +1892,95 @@ describe('통합 WAL 단계별 resultOid 규칙(Codex 2R P1-E)', () => {
expect(r.kind).toBe('invariant-violation')
})

it('abandoned 는 resultOid 를 요구하지 않는다', async () => {
/**
* **CodeRabbit PR#289** — 「최초 레코드가 완결 귀속을 들고 태어나는」 축에 행이 없었다. 전이 계층은
* `prev === undefined` 에서 lifecycle 만 보므로(정정 227) 그 조합을 막는 것은 **단일 레코드 불변식
* ②**(`completedIntegrationTxnId` 존재 ⟺ `lifecycle==='integrated'`)다. 이 세션의 규율대로
* **「다른 층이 막는다」를 주석이 아니라 행으로** 남긴다 — 규칙 이름을 문면까지 단언한다.
*/
it('최초 CAS 는 완결 상태를 직접 만들 수 없다(②와 정정 227 이 각각 답한다)', async () => {
// ⓐ `open` + 완결 귀속 → 불변식 ② 가 답한다.
const openWithCompleted = await casWith({ completedIntegrationTxnId: 'T1' })
expect(openWithCompleted.kind).toBe('invariant-violation')
expect(
openWithCompleted.kind === 'invariant-violation' && openWithCompleted.violations.join(),
).toContain('②')

// ⓑ `integrated` + 완결 귀속 → 전이 계층(정정 227)이 답한다.
const integrated = await casWith({
lifecycle: 'integrated',
completedIntegrationTxnId: 'T1',
})
expect(integrated.kind).toBe('invariant-violation')
expect(integrated.kind === 'invariant-violation' && integrated.violations.join()).toContain(
'최초 레코드의 lifecycle 은 open',
)
})

/**
* ⚠ **기대가 뒤집혔다**(Codex PR#289 P1 의 파급 · 은폐하지 않는다). 이전 판은 첫 CAS 로
* `abandoned` 레코드를 만들어 「resultOid 를 요구하지 않는다」를 보였는데, 계획 정정 177 은
* **권위 레코드가 `abandoned` 를 갖지 않는다**고 못박았다 — 포기는 단계를 남기는 게 아니라 통합
* 4필드를 **소거**하는 것이다. 즉 이전 기대는 **어떤 생산자도 만들지 않는 상태**를 정상으로
* 고정하고 있었고, 「최초는 prepared 로만」이 서면서 그 모순이 드러났다.
*/
it('abandoned 는 권위 레코드가 가질 수 있는 단계가 아니다(정정 177)', async () => {
const r = await casWith({
currentIntegrationTxnId: 'T1',
currentIntegrationStage: 'abandoned',
currentIntegrationTxnGeneration: 1,
})
expect(r.kind).toBe('committed')
expect(r.kind).toBe('invariant-violation')
expect(r.kind === 'invariant-violation' && r.violations.join()).toContain(
'abandoned 를 가질 수 없다',
)
})
})

/* ================================================================================================
* draft 스냅숏 — **검증한 값과 착지한 값은 같은 값**이어야 한다 (Codex PR#289 8R P1)
* ============================================================================================= */

describe('CAS 는 제출 draft 를 한 번만 관측한다(전이 검증의 전제)', () => {
/**
* **Codex PR#289 8R P1** — `runCas` 는 `next` 를 ③사전조건·④b `checkTransitionInvariants`·
* `serialize` 에서 **각각 다시 읽는다**. 평범한 객체는 안전하지만 getter·`Proxy` 는 층마다 다른 값을
* 줄 수 있고, 유일한 사후 관문인 왕복 검증은 `checkInvariants`(**단일 레코드**)만 태우므로
* **WAL 단계를 건너뛴 레코드가 커밋된다** — 전이 계층이 본 것과 디스크에 착지한 것이 갈린다.
*
* 재현: 최초 CAS(권위 부재)에서 `currentIntegrationStage` 가 전이 검증 때는 `prepared`(정정 227 이
* 허용하는 유일한 시작), 직렬화 때는 `published` 를 답한다.
*/
it('getter 가 층마다 다른 값을 줘도 검증한 값이 그대로 착지한다', async () => {
const fx = await setup()
let reads = 0
const shifty = {
schemaVersion: 1,
identity: { commonGitDir: COMMON_GIT_DIR, benchRoot: BENCH_ROOT, benchId: fx.benchId },
lifecycle: 'open',
sourceGeneration: 1,
currentIntegrationTxnId: 'T1',
currentIntegrationTxnGeneration: 1,
currentIntegrationResultOid: 'a'.repeat(40),
get currentIntegrationStage() {
reads += 1
// 첫 관측만 합법(`prepared`), 그 뒤로는 단계를 건너뛴 값을 답한다.
return reads <= 1 ? 'prepared' : 'published'
},
} as unknown as BenchAuthorityDraft

const r = await fx.store.withAuthority(fx.lease, async (tx) => {
const read = tx.readFresh()
if (read.kind !== 'absent') throw new Error('absent 예상')
return tx.compareAndSwap(read.read, shifty)
})

// 커밋되든 거부되든 **디스크에 단계를 건너뛴 레코드가 남아서는 안 된다.**
if (r.kind === 'committed') {
const landed = JSON.parse(fx.fs.readFileUtf8(fx.path)) as BenchAuthorityRecord
expect(landed.currentIntegrationStage).toBe('prepared')
} else {
expect(r.kind).toBe('invariant-violation')
}
})
})
Loading