Skip to content

Commit 32cde97

Browse files
committed
기능: 이름 있는 내구 가지와 시도 경쟁 동사로 상태 분기를 1급으로 올린다
저널은 이미 git의 오브젝트 모델(내용주소 blob/tree/commit, HEAD/PREV refs)을 말한다. 빠져 있던 것은 어휘다: 시도를 남기고 비교하고 채택하는 동사와, 왜 이 상태를 택했는지의 기록. state 층: refProtocol의 쓰기 순서 법에 (5') 가지 교체를 더한다(assertRefName, PREV 직접 커밋 금지, 가지 판독은 후퇴 없이 명시 예외). commitState가 ref를, openState가 { ref }를 받고, store 계약에 listRefs/removeRef가 는다(Memory/Opfs/JournalKernel 세 드라이버). objectModel의 commit.env에 note(provenance, canonical JSON 4KB 상한)가 실린다. journal 층: commitBranch(HEAD/PREV 불변, parents=갈림점) / listBranches / recoverBranch (h0 대조 동일) / adoptBranch(가지 물질화 후 HEAD 커밋, parents=가지 커밋, note.adoptedFrom) / deleteBranch. live 판정이 가지 세대를 걷어 pack/prune이 가지를 지운다. 가지가 존재하면 마커가 v2가 되어 구 pyproc recover가 fail-closed하고(구 버전 prune이 못 보는 가지 blob을 지우는 유실 차단), 마지막 가지 삭제가 v1을 복원한다. 힙 상태는 병합이 성립하지 않으므로 merge는 없다: 가지의 소비 동사는 adopt다. porcelain: history.branch/branches/recoverBranch/adopt/deleteBranch + commit({ note })와, 휘발 구역의 history.attempts(codes): 같은 기반에서 후보를 직렬로 돌려 형제 가지로 남기고 사이마다 기반으로 되감아 실패가 다음 시도를 오염시키지 못하며 adopt(i)가 승자를 복원한다. 직렬인 이유는 restoreLive가 ms 단위로 싸고 병렬(fork)은 시도 수만큼 힙을 사기 때문이다. 내구 핸들(KernelElection)은 이번 범위 밖이다(선출 fence와의 결합은 별도 판단). 실측: tests/attempts/stateKernel/branchRefsProbe.html 10/10 GREEN(실 OPFS + 실 힙 전 수명주기). 게이트 하한 인상: [state 커널] 12->14, 브라우저 gate.html 112->133. 검증: npm test 3374 passed 0 failed, test:browser 133/133 연속 3회, test:installed 37/37, test:golden 8/8, test:types 0, test:package ok, test:contracts 7 suites. 신설 게이트 음성 시험 6종 통과: 가지 커밋이 HEAD를 움직이게 하면 RED, note 상한 제거 RED, 파손 가지 첫 부팅 위장 RED, liveKeys에서 가지를 빼면 prune 후 부활이 명시 파손으로 RED (조용한 유실 아님까지 실증), 마커 상시 v1이면 RED, attempts의 기반 복원을 빼면 격리 판정 RED.
1 parent 51e1f94 commit 32cde97

15 files changed

Lines changed: 512 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@ happen only on an explicit maintainer decision; the Unreleased section accumulat
88

99
## Unreleased
1010

11+
### Added
12+
13+
- **Named durable branches with provenance, and an attempts verb for competing solutions.** The
14+
journal already speaks git's object model (content-addressed blob/tree/commit, HEAD/PREV refs);
15+
this names the missing layer. `history.branch(name, { dir, note })` commits the current state to
16+
a branch ref without touching HEAD, `history.branches()` lists them with their fork parents and
17+
notes, `history.recoverBranch(name)` materializes one, and `history.adopt(name)` makes a branch
18+
the machine state and the new HEAD - the adopting commit's parents point at the branch commit and
19+
its note records `{ adoptedFrom }`. Heap states cannot be merged, so the consuming verb is adopt,
20+
not merge. Commits accept `note` (canonical JSON, 4 KB cap): what was tried and why a state was
21+
chosen lives in the same object as the state itself.
22+
23+
In the volatile zone, `history.attempts([codes])` runs candidates serially from one base, each
24+
attempt checkpointed as a sibling branch with the heap rewound in between, so a failing attempt
25+
cannot contaminate the next; `adopt(i)` restores the winner. Serial on purpose: restoreLive is
26+
cheap, while parallel attempts cost one heap each.
27+
28+
**Compatibility**: a journal carrying branches marks itself format version 2, and an older
29+
pyproc refuses to recover it (fail closed) rather than pruning branch data its live-set walk
30+
cannot see. Do not run an older pyproc's `pack()`/`prune()` directly against a branched journal.
31+
Deleting the last branch restores version 1. Journals without branches are unchanged.
32+
1133
<!-- unreleased-subpaths: -->
1234
소비자가 핀한 버전에 아직 없는 subpath 목록이다(위 주석이 기계 판독 정본). 출하 문서가 이 이름을
1335
예시로 쓰면 미출하 표식이 함께 있어야 하고, tests/contracts/publicSurface.mjs가 그것을 문다.
@@ -28,6 +50,7 @@ migration. Pin `0.0.13`.
2850
레인이 자체 호스팅 엔진을 안 받아 Python guest를 올리는 probe가 죽었고, 그 배관을 고치면서
2951
"브라우저를 띄우는 job은 같은 job에서 엔진을 마련한다"를 게이트로 세웠다. **패키지 내용은
3052
0.0.12가 냈을 것과 같다.** 아래 0.0.12 절의 브레이킹과 이관 안내가 이 릴리즈의 내용이다.
53+
3154
## 0.0.12 - 2026-08-03
3255

3356
### Breaking

index.d.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,10 @@ export interface JournalAutoPackPolicy {
720720
}
721721

722722
export interface JournalCommitResult {
723+
/** Which ref this commit moved: "HEAD", or "branch-<name>" for a branch commit. */
724+
ref: string;
725+
/** The content address of the commit object this generation is anchored at. */
726+
commit: string;
723727
pages: number;
724728
wrote: number;
725729
mb: number;
@@ -785,8 +789,23 @@ declare class MachineJournal {
785789
/** Starts the idle watcher. It never interrupts running code. */
786790
start(): MachineJournal;
787791
stop(): void;
788-
/** Commits the current state at a manual boundary. Returns the changed page count and the bytes actually written after dedupe. */
789-
commit(): Promise<JournalCommitResult | null>;
792+
/** Commits the current state at a manual boundary. Returns the changed page count and the bytes actually written after dedupe. opts.note rides the commit object as provenance. */
793+
commit(opts?: { note?: Record<string, unknown> }): Promise<JournalCommitResult | null>;
794+
/**
795+
* Commits the current state to a named branch ref. HEAD and PREV are untouched, and the commit's
796+
* parents record the fork point (the HEAD commit at branch time). Heap states cannot be merged,
797+
* so the consuming verb is adopt, not merge. A journal carrying branches marks itself version 2:
798+
* an older pyproc refuses it (fail closed) instead of pruning branch data it cannot see.
799+
*/
800+
commitBranch(name: string, opts?: { note?: Record<string, unknown> }): Promise<JournalCommitResult | null>;
801+
/** Lists branch refs with their commit address, creation time, provenance note, and fork parents. */
802+
listBranches(): Promise<Array<{ name: string; commit: string; createdAt: string | null; note: Record<string, unknown> | null; parents: string[] }>>;
803+
/** Materializes a branch generation onto the heap (same h0 contract as recover). Null when the branch does not exist; corruption is an explicit error, never a first-boot masquerade. */
804+
recoverBranch(name: string): Promise<JournalRecoverResult | null>;
805+
/** Materializes the branch and commits that state to HEAD. The adopting commit's parents point at the branch commit and its note carries { adoptedFrom } plus opts.note. */
806+
adoptBranch(name: string, opts?: { note?: Record<string, unknown> }): Promise<(JournalCommitResult & { adopted: string; applied: JournalRecoverResult }) | null>;
807+
/** Removes the branch ref. Blob reclamation is the next prune's job. Deleting the last branch restores marker version 1 (older-version compatibility). */
808+
deleteBranch(name: string): Promise<{ deleted: string }>;
790809
/** Removes journal generations and leaves a deleted tombstone so intentional deletion is not reported as eviction. */
791810
delete(): Promise<JournalDeleteResult>;
792811
/** Bundles only the HEAD/PREV live blobs into one pack file and reduces loose and stale files. */
@@ -1262,8 +1281,30 @@ declare class PyprocHistory {
12621281
prune(target?: number | CheckpointInfo): { freedNodes: number; freedMB: number; keptNodes: number };
12631282
stats(): ReactiveStats;
12641283
setRetentionPolicy(policy: ReactiveRetentionPolicy | null): Readonly<ReactiveRetentionPolicy> | null;
1265-
/** Kernel commit through the WAL journal. The same dir shares one journal instance. */
1266-
commit(opts: { dir: FileSystemDirectoryHandle } & Omit<JournalConfig, "reactive" | "dir">): Promise<JournalCommitResult | null>;
1284+
/**
1285+
* Runs candidate codes serially from the current state, each attempt checkpointed as a sibling
1286+
* branch of the same base and the heap rewound to the base in between, so a failing attempt
1287+
* cannot contaminate the next one. Ends at the base state; adopt(i) restores attempt i's end
1288+
* state. Serial on purpose: restoreLive is cheap, while parallel attempts cost one heap each
1289+
* (that is what proc().map is for).
1290+
*/
1291+
attempts(codes: string[]): {
1292+
base: CheckpointInfo;
1293+
attempts: Array<{ index: number; code: string; ok: boolean; value: unknown; error: unknown; checkpoint: CheckpointInfo }>;
1294+
adopt(index: number): { index: number; code: string; ok: boolean; value: unknown; error: unknown; checkpoint: CheckpointInfo };
1295+
};
1296+
/** Kernel commit through the WAL journal. The same dir shares one journal instance. opts.note rides the commit as provenance. */
1297+
commit(opts: { dir: FileSystemDirectoryHandle; note?: Record<string, unknown> } & Omit<JournalConfig, "reactive" | "dir">): Promise<JournalCommitResult | null>;
1298+
/** Commits the current state to a named durable branch (HEAD untouched). See MachineJournal.commitBranch. */
1299+
branch(name: string, opts: { dir: FileSystemDirectoryHandle; note?: Record<string, unknown> } & Omit<JournalConfig, "reactive" | "dir">): Promise<JournalCommitResult | null>;
1300+
/** Lists durable branches with provenance notes and fork parents. */
1301+
branches(opts: { dir: FileSystemDirectoryHandle } & Omit<JournalConfig, "reactive" | "dir">): Promise<Array<{ name: string; commit: string; createdAt: string | null; note: Record<string, unknown> | null; parents: string[] }>>;
1302+
/** Materializes a branch generation onto the heap. */
1303+
recoverBranch(name: string, opts: { dir: FileSystemDirectoryHandle } & Omit<JournalConfig, "reactive" | "dir">): Promise<JournalRecoverResult | null>;
1304+
/** Adopts a branch: its state becomes the machine state and the new HEAD, with provenance recording what was adopted and why. */
1305+
adopt(name: string, opts: { dir: FileSystemDirectoryHandle; note?: Record<string, unknown> } & Omit<JournalConfig, "reactive" | "dir">): Promise<(JournalCommitResult & { adopted: string; applied: JournalRecoverResult }) | null>;
1306+
/** Deletes a branch ref. */
1307+
deleteBranch(name: string, opts: { dir: FileSystemDirectoryHandle } & Omit<JournalConfig, "reactive" | "dir">): Promise<{ deleted: string }>;
12671308
/** Explicitly deletes the journal and leaves a tombstone so intentional absence is not reported as eviction. */
12681309
delete(opts: { dir: FileSystemDirectoryHandle } & Omit<JournalConfig, "reactive" | "dir">): Promise<JournalDeleteResult>;
12691310
recover(opts: { dir: FileSystemDirectoryHandle } & Omit<JournalConfig, "reactive" | "dir">): Promise<JournalRecoverResult | null>;

src/capabilities/journal/journalKernelStore.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ export class JournalKernelStore {
5656
return refs.readRef(name);
5757
}
5858
async writeRef(name, ref) { return (await this._refs(true)).writeRef(name, ref); }
59+
async listRefs() {
60+
const refs = await this._refs(false);
61+
return refs ? refs.listRefs() : [];
62+
}
63+
async removeRef(name) {
64+
const refs = await this._refs(false);
65+
if (refs) await refs.removeRef(name);
66+
}
5967
// 저널은 단일 컨트롤러 경로다(kernelElection의 Web Locks가 단일성을 구조 보장). fence 없음.
6068
async readOwner() { return null; }
6169
}

0 commit comments

Comments
 (0)