Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2b111ec
fix(Alden): reject reversed or malformed activity pages before advanc…
twoimo Oct 10, 2026
3bdee3a
fix(Alden): retain history checkpoint on malformed or unordered respo…
twoimo Oct 10, 2026
dabd5c0
test(Alden): verify out-of-order and malformed graph events retain cu…
twoimo Oct 10, 2026
055a828
test(Alden): ensure memory history does not skip reordered or malform…
twoimo Oct 10, 2026
91371a4
test(Alden): preserve typed read args and event TTL during cursor ret…
twoimo Oct 10, 2026
7013eca
feat(Alden): verify current source/index lineage on expanded memory-h…
twoimo Oct 10, 2026
7b33c3f
feat(Alden): persist target-specific previous version in immutable st…
twoimo Oct 10, 2026
d661a78
feat(Alden): expose previous version and fence lazy proof after journ…
twoimo Oct 10, 2026
c3ed764
test(Alden): preserve source-specific before/after versions without c…
twoimo Oct 10, 2026
c669bce
test(Alden): validate memory-history lazy source readback, identity a…
twoimo Oct 10, 2026
00c3c79
fix(Alden): narrow nullable evidence reply before reading verified fi…
twoimo Oct 10, 2026
5feb8ea
fix(Alden): distinguish stored additions, revisions, removals and rel…
twoimo Oct 10, 2026
1593f42
test(Alden): show accurate stage ledger counts and avoid unnecessary …
twoimo Oct 10, 2026
7da0e35
docs(Alden): record durable activity cursor guards and scoped memory …
twoimo Oct 10, 2026
4a36eff
docs(Alden): record ordered memory events, lazy proof readback and ma…
twoimo Oct 10, 2026
297e54a
docs(Alden): record ordered memory events, lazy proof readback and ma…
twoimo Oct 10, 2026
7b280a9
docs(Alden): record ordered memory events, lazy proof readback and ma…
twoimo Oct 10, 2026
7c4abb9
docs(Alden): record ordered memory events, lazy proof readback and ma…
twoimo Oct 10, 2026
c62d6b7
docs(Alden): clarify external osk-system validation gate
twoimo Oct 10, 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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Alden

## 2026-10-11 — 이벤트 순서 복구 및 ‘기억 정리’ 재조회 (미설치 검토 브랜치)

PR #29의 통합 그래프 위에서 영속 수집 이벤트의 손상·역전·중복 페이지를
완료 처리하지 않고 동일 커서로 재조회하도록 개선했습니다.
‘기억 정리’에서 저장 단계의 추가·수정·유지·원본 제외·관계 변경을
구분하고, 개정 전 버전과 저장·원문 SHA-256·FTS·Dense 바인딩 상태를
선택한 이력 한 건에서만 확인합니다. 조회로 새 기억이나 3D 활동을
합성하지 않으며 늦은 답과 다른 프로젝트·대상의 근거는 표시하지 않습니다.

[구현 계약·복구·검증 근거](docs/architecture/alden-activity-readback-20261011.md) ·
[macOS CI 38070713835](https://github.com/twoimo/alden/actions/runs/38070713835):
Python 60개, UI 471개, TypeScript·Vite 빌드 통과.
원본 Mac의 `outputs/STATUS.md`, 실제 OSK 호스트/모델/마이크, 사용자 설치본 및
서명·공증 배포는 이번 원격 결과로 검증되지 않았습니다.

## 2026-10-11 개발 브랜치 — 문서 한 건의 통합 증거 대사

[구현·스코프·복구·CI 상세](docs/architecture/alden-pipeline-readback-20261011.md).
Expand Down
18 changes: 18 additions & 0 deletions desktop/DESIGN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Alden desktop design contract

## 2026-10-11 — 저장 원본 재조회와 실시간 이벤트의 분리

‘기억 정리’의 한 행은 대상·항목·시각·확정 단계를 간결하게 표시합니다.
저장 항목을 펼쳐 사용자 요청으로만 현재 원본 SHA-256·FTS·Dense 저장
바인딩을 조회하며, 선택한 `project + document + version + target + run`이
일치한 결과에만 확인 문구를 부여합니다. 개정 전 버전과 원본 제외,
관계 갱신·미확인·대기 상태는 저장 완료와 시각적으로 구분합니다.

3D 활동 이벤트는 유효한 journal stream/cursor의 오름차순 저장
이벤트만 소비합니다. 역전/손상 페이지는 앞선 장면/커서를 보존하며 재조회하고
과거 저장·클라우드 추론·사용자 조회를 새로운 지식 신호로 꾸미지 않습니다.
숨김/재연결 때 늦은 결과는 현재 화면에 반영하지 않습니다. 기존
검은 은하형 그래프 색·위치·에너지 표현과 목록의 평온한 디자인은 유지합니다.

[정확한 실행·UI·원문 확인 계약](../docs/architecture/alden-activity-readback-20261011.md).
macOS CI의 UI 471개는 DOM·로직 검증이며 실제 사용자 설치본의
GPU 프레임·물리 입력·서명/공증 증거는 아닙니다.

## 2026-10-11 — Evidence in the real 3D note view (source branch)

The selected source detail retains the calm existing note layout and a single
Expand Down
21 changes: 21 additions & 0 deletions desktop/src/__tests__/collection-activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ describe('committed graph activity consumption', () => {
await vi.advanceTimersByTimeAsync(2500); consumer.snapshot(point(16), nodes());
expect(show).not.toHaveBeenCalled(); expect(consumer.diagnostics().pending).toBe(0);
});
it('retries a reversed, failed or duplicate page without consuming its cursor or pulsing', async () => {
const read = vi.fn(async (_: Record<string, unknown>) => page(12, [event(11), event(12)]))
.mockResolvedValueOnce(page(12, [event(12), event(11)]))
.mockResolvedValueOnce(page(12, [event(11, { success: false } as never), event(12)]))
.mockResolvedValueOnce(page(12, [event(11), event(12, { event_id: 'event-11' })]));
const { consumer, refresh, show } = setup(read);
for (let attempt = 0; attempt < 3; attempt++) {
await vi.advanceTimersByTimeAsync(2500);
expect(consumer.diagnostics().cursor).toBe(10);
expect(consumer.diagnostics().seen).toBe(0);
expect(consumer.diagnostics().pending).toBe(0);
expect(refresh).not.toHaveBeenCalled();
expect(show).not.toHaveBeenCalled();
expect(read.mock.calls.at(-1)![0]).toMatchObject({ after: 10, stream_id: 'stream-a' });
}
await vi.advanceTimersByTimeAsync(2500);
expect(consumer.diagnostics().cursor).toBe(12);
expect(refresh).toHaveBeenCalledTimes(1);
consumer.snapshot(point(12), nodes());
expect(show).toHaveBeenCalledExactlyOnceWith(event(12));
});
it('fences hidden replies and resumes the old cursor without lighting hidden history', async () => {
let resolve!: (value: ReturnType<typeof page>) => void;
const read = vi.fn((_options: Record<string, unknown>) => new Promise<ReturnType<typeof page>>(done => { resolve = done; }));
Expand Down
103 changes: 103 additions & 0 deletions desktop/src/__tests__/collection-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ describe('durable collection history', () => {
expect(document.querySelector<HTMLButtonElement>('.history-older')!.hidden).toBe(true);
expect(vi.getTimerCount()).toBe(1);
});
it('separates successful storage, removal and relation changes in the saved ledger', async () => {
const history = [event(5,{details:{change:'relations_changed'}}),
event(4,{details:{change:'removed'}}), event(3,{details:{change:'unchanged'}}),
event(2,{details:{change:'revised'}}),event(1,{details:{change:'added'}})];
const totals = { total:5, stages:{ discovered:0,parsed:0,validated:0,stored:5,indexed:0,failed:0,paused:0 },
changes:{added:1,revised:1,unchanged:1,removed:1,relations_changed:1} };
const load = vi.fn<typeof fetchSettingsAction>().mockResolvedValue({ ...response(history),summary:totals });
setup(load); await vi.advanceTimersByTimeAsync(0);
const labels = [...document.querySelectorAll('.collection-stage')].map(badge => badge.textContent);
expect(labels).toEqual(['관계 갱신','원본에서 제외','기존 자료 유지','기존 자료 수정','새 자료 저장']);
expect(document.querySelector('.collection-summary')!.textContent).toContain('제외 1 · 관계 갱신 1');
expect(load.mock.calls.filter(([action]) => action === 'collection-trace')).toHaveLength(0);
});
it('describes validation and unchanged records truthfully and never links executable URLs', async () => {
const load = vi.fn<typeof fetchSettingsAction>().mockResolvedValue(response([
event(3, { stage: 'validated', source_url: 'javascript:alert(1)' }),
Expand Down Expand Up @@ -109,6 +122,36 @@ describe('durable collection history', () => {
expect(vi.getTimerCount()).toBe(1);
});

it('rejects a reversed forward page and retries it without losing earlier history', async () => {
const load = vi.fn<typeof fetchSettingsAction>()
.mockResolvedValueOnce({ ...response([event(3)]), stream_id: 'stable', cursor: 3 })
.mockResolvedValueOnce({ ...response([event(5), event(4)]), stream_id: 'stable', cursor: 5 })
.mockResolvedValueOnce({ ...response([event(4), event(5)]), stream_id: 'stable', cursor: 5 });
setup(load); await vi.advanceTimersByTimeAsync(0);
expect(document.querySelector('.collection-event-list')!.textContent).toContain('자료 3');
await vi.advanceTimersByTimeAsync(2500);
expect(query(load).after).toBe(3);
expect(document.querySelector('.collection-status')!.textContent).toContain('순서를 확인하지 못했습니다');
expect(document.querySelector('.collection-event-list')!.textContent).not.toContain('자료 5');
expect(document.querySelector('.collection-event-list')!.textContent).toContain('자료 3');
await vi.advanceTimersByTimeAsync(5000);
expect(query(load).after).toBe(3);
const history = document.querySelector('.collection-event-list')!.textContent!;
expect(history).toContain('자료 3'); expect(history).toContain('자료 4'); expect(history).toContain('자료 5');
expect(document.querySelectorAll('.collection-event-row')).toHaveLength(3);
});
it('keeps the entire initial page uncommitted when any item is invalid', async () => {
const load = vi.fn<typeof fetchSettingsAction>()
.mockResolvedValueOnce(response([event(3), { ...event(2), sequence: 0 }, event(1)]))
.mockResolvedValueOnce(response([event(3), event(2), event(1)]));
setup(load); await vi.advanceTimersByTimeAsync(0);
expect(document.querySelector('.collection-event-list')!.textContent).not.toContain('자료 3');
expect(document.querySelector('.collection-status')!.textContent).toContain('순서를 확인하지 못했습니다');
await vi.advanceTimersByTimeAsync(5000);
expect(query(load).after).toBeUndefined();
expect(document.querySelector('.collection-event-list')!.textContent).toContain('자료 3');
expect(document.querySelectorAll('.collection-event-row')).toHaveLength(3);
});
it('keeps the older view stable while announcing new durable stages and allows returning to latest', async () => {
const load = vi.fn<typeof fetchSettingsAction>()
.mockResolvedValueOnce(response([event(20)], 20))
Expand All @@ -127,6 +170,66 @@ describe('durable collection history', () => {
expect(document.querySelector('.collection-event-list')!.textContent).toContain('자료 21');
});

it('reads exact current source proof on demand and displays its own previous version', async () => {
const document_id = 'youtube:' + 'a'.repeat(64), version = 'version:' + 'b'.repeat(64);
const previous_version = 'version:' + 'c'.repeat(64), target_id = 'target-a';
const saved = event(8, { event_id: 'event-8', document_id, version, target_id,
run_id: 'run-8', details: { change: 'revised', previous_version } });
const proof = { ok: true, state: 'available', projects: ['one'], document_id, version, target_id,
run_id: 'run-8', source: { state: 'hash_verified' },
stages: { stored: true, indexed: true }, fts: { state: 'verified' },
dense: { state: 'stored_vector_binding_verified' } };
const load = vi.fn<typeof fetchSettingsAction>().mockImplementation(async action =>
action === 'collection-trace' ? proof : response([saved]));
setup(load); await vi.advanceTimersByTimeAsync(0);
expect(load.mock.calls.filter(([action]) => action === 'collection-trace')).toHaveLength(0);
const detail = document.querySelector<HTMLDetailsElement>('.collection-event-detail')!;
detail.open = true; detail.dispatchEvent(new Event('toggle')); await vi.advanceTimersByTimeAsync(0);
const requests = load.mock.calls.filter(([action]) => action === 'collection-trace');
expect(requests).toHaveLength(1);
expect(JSON.parse(requests[0][1]!.query!)).toEqual({
projects: ['one'], document_id, expected_version: version, target_id,
});
expect(detail.textContent).toContain('변경 전 버전');
expect(detail.textContent).toContain(previous_version);
expect(detail.querySelector('.collection-readback')!.textContent).toContain('저장·원문 해시 확인');
expect(detail.querySelector('.collection-readback')!.textContent).toContain('FTS 확인');
expect(detail.querySelector('.collection-readback')!.textContent).toContain('Dense 저장 바인딩 확인');
expect(detail.querySelector('.collection-readback')!.textContent).toContain('모델 실행은 별도 검증');
});
it('rejects mismatched trace identity and never presents readback as new knowledge', async () => {
const document_id = 'youtube:' + 'a'.repeat(64), version = 'version:' + 'b'.repeat(64), target_id = 'target-a';
const saved = event(8, { document_id, version, target_id, run_id: 'run-8' });
const load = vi.fn<typeof fetchSettingsAction>().mockImplementation(async action =>
action === 'collection-trace' ? { ok:true,state:'available',projects:['one'],document_id,
version,target_id:'different-target',run_id:'run-8',
stages:{stored:true,indexed:true},source:{state:'hash_verified'},fts:{state:'verified'} }
: response([saved]));
setup(load); await vi.advanceTimersByTimeAsync(0);
const detail = document.querySelector<HTMLDetailsElement>('.collection-event-detail')!;
detail.open = true; detail.dispatchEvent(new Event('toggle')); await vi.advanceTimersByTimeAsync(0);
expect(detail.querySelector('.collection-readback')!.textContent).toBe('저장 결과를 확인하지 못했습니다.');
expect(document.querySelector('.collection-summary')!.textContent).not.toContain('새로운 기억');
expect(load.mock.calls.filter(([action]) => action === 'collection-trace')).toHaveLength(1);
});
it('drops a late trace reply when history is hidden before the readback completes', async () => {
const document_id = 'youtube:' + 'a'.repeat(64), version = 'version:' + 'b'.repeat(64), target_id = 'target-a';
const saved = event(8, { document_id, version, target_id, run_id: 'run-8' });
let resolve!: (value: Record<string, unknown>) => void;
const load = vi.fn<typeof fetchSettingsAction>().mockImplementation(action =>
action === 'collection-trace' ? new Promise(done => { resolve = done; })
: Promise.resolve(response([saved])));
const history = setup(load); await vi.advanceTimersByTimeAsync(0);
const detail = document.querySelector<HTMLDetailsElement>('.collection-event-detail')!;
detail.open = true; detail.dispatchEvent(new Event('toggle')); await vi.advanceTimersByTimeAsync(0);
const readback = detail.querySelector<HTMLElement>('.collection-readback')!;
history.visible(false);
resolve({ok:true,state:'available',projects:['one'],document_id,version,target_id,run_id:'run-8',
stages:{stored:true,indexed:true},source:{state:'hash_verified'},fts:{state:'verified'}});
await vi.advanceTimersByTimeAsync(0);
expect(readback.textContent).not.toContain('원문 해시 확인');
expect(load.mock.calls.filter(([action]) => action === 'collection-trace')).toHaveLength(1);
});
it('passes project, target, source, stage and local date boundaries to the read-only API', async () => {
const load = vi.fn<typeof fetchSettingsAction>().mockResolvedValue(response([]));
setup(load); await vi.advanceTimersByTimeAsync(0);
Expand Down
Loading