From 2b111ecd00cb0454e20195bd520ed0cbb636c88f Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:59:44 +0900 Subject: [PATCH 01/19] fix(Alden): reject reversed or malformed activity pages before advancing cursor --- desktop/src/knowledge/collection-activity.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/desktop/src/knowledge/collection-activity.ts b/desktop/src/knowledge/collection-activity.ts index fee78915..1f273ea2 100644 --- a/desktop/src/knowledge/collection-activity.ts +++ b/desktop/src/knowledge/collection-activity.ts @@ -111,11 +111,21 @@ export class CollectionActivity { } if (page.reset !== false || !integer(page.latest) || next.cursor > page.latest || !Array.isArray(page.items) || page.items.length > 200 || typeof page.has_more !== 'boolean' || page.has_more && next.cursor === before.cursor) return; - let previous = before.cursor, needsSnapshot = false; + // Validate the entire ordered page before consuming any receipt or + // moving the journal cursor. Otherwise a reversed/corrupt row can be + // silently skipped while a later cursor makes that loss permanent. + const staged: Array = []; + const pageIds = new Set(); + let previous = before.cursor; for (const raw of page.items) { const event = receipt(raw); - if (!event || event.sequence <= previous || event.sequence > next.cursor || this.seen.has(event.event_id)) continue; - previous = event.sequence; needsSnapshot ||= event.sequence > this.snapshotCursor; this.seen.add(event.event_id); + if (!event || event.sequence <= previous || event.sequence > next.cursor + || pageIds.has(event.event_id) || this.seen.has(event.event_id)) return; + staged.push(event); pageIds.add(event.event_id); previous = event.sequence; + } + let needsSnapshot = false; + for (const event of staged) { + needsSnapshot ||= event.sequence > this.snapshotCursor; this.seen.add(event.event_id); if (this.seen.size > 400) this.seen.delete(this.seen.values().next().value!); if (event.kind === 'removed' || event.kind === 'relations_changed') continue; const now = this.now(), at = event.at * 1000; From 3bdee3a6570e0cb149448cefe78da1fdbce8fc28 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:00:02 +0900 Subject: [PATCH 02/19] fix(Alden): retain history checkpoint on malformed or unordered response pages --- desktop/src/collection-history.ts | 33 ++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/desktop/src/collection-history.ts b/desktop/src/collection-history.ts index ed25b0ae..a55d63fd 100644 --- a/desktop/src/collection-history.ts +++ b/desktop/src/collection-history.ts @@ -9,8 +9,24 @@ const stages: Record = { }; const rows = (value: unknown): Row[] => Array.isArray(value) ? value.filter(v => v && typeof v === 'object' && !Array.isArray(v)) as Row[] : []; -const validEvents = (value: unknown): Row[] => rows(value).filter(row => - Number.isSafeInteger(row.sequence) && Number(row.sequence) > 0); +// The server delivers newest-first initial/older pages and oldest-first +// forward pages. A malformed or reordered page is retried intact; dropping a +// row and then committing the newer cursor would erase durable history from +// this view until a manual reset. +const validEvents = (value: unknown, ascending: boolean): Row[] | null => { + if (!Array.isArray(value) || value.length > 200) return null; + const result: Row[] = []; + let previous: number | null = null; + for (const item of value) { + if (!item || typeof item !== 'object' || Array.isArray(item)) return null; + const row = item as Row, sequence = row.sequence; + if (!Number.isSafeInteger(sequence) || Number(sequence) <= 0) return null; + const current = Number(sequence); + if (previous !== null && (ascending ? current <= previous : current >= previous)) return null; + result.push(row); previous = current; + } + return result; +}; const WINDOW_LIMIT = 1000; const changes = ['added', 'revised', 'unchanged', 'removed', 'relations_changed'] as const; type Counts = { total: number; stages: Record; changes: Record }; @@ -162,16 +178,23 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe const result = await load('collection-history', { query: JSON.stringify(options) }); if (dead || ticket !== epoch || !visible || page !== 'history') return; if (result?.ok !== true) { section.dataset.state = 'error'; status.textContent = '수집 이력을 불러오지 못했습니다.'; delay = 5000; return; } - section.dataset.state = result.state === 'not_configured' ? 'not_configured' : 'ready'; - populate(project, result.projects, 'project', 'project'); populate(target, result.targets, 'id', 'label'); const nextStream = typeof result.stream_id === 'string' && result.stream_id.length <= 128 ? result.stream_id : null; const restarting = result.reset === true || nextStream !== null && stream !== null && stream !== nextStream; + const verified = validEvents(result.items, forward && !restarting); + if (verified === null) { + section.dataset.state = 'error'; + status.textContent = '수집 이력의 순서를 확인하지 못했습니다. 같은 범위를 다시 조회합니다.'; + delay = 5000; + return; + } + section.dataset.state = result.state === 'not_configured' ? 'not_configured' : 'ready'; + populate(project, result.projects, 'project', 'project'); populate(target, result.targets, 'id', 'label'); if (restarting) { items = []; newest = before = null; historical = false; newCount = 0; expanded.clear(); aggregate = null; } if (nextStream) stream = nextStream; const append = forward && !restarting; - const incoming = validEvents(result.items).filter(row => !append || Number(row.sequence) > (newest ?? 0)); + const incoming = verified.filter(row => !append || Number(row.sequence) > (newest ?? 0)); const snapshotCounts = counts(result.summary); if (snapshotCounts) aggregate = snapshotCounts; else if (append && aggregate) for (const row of incoming) { From dabd5c07f381f30a08f474ca324593aefc5d297b Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:00:33 +0900 Subject: [PATCH 03/19] test(Alden): verify out-of-order and malformed graph events retain cursor and retry --- .../src/__tests__/collection-activity.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/desktop/src/__tests__/collection-activity.test.ts b/desktop/src/__tests__/collection-activity.test.ts index 5a81babd..e2b363a5 100644 --- a/desktop/src/__tests__/collection-activity.test.ts +++ b/desktop/src/__tests__/collection-activity.test.ts @@ -74,6 +74,28 @@ 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 () => 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); + vi.setSystemTime(12000); + 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) => void; const read = vi.fn((_options: Record) => new Promise>(done => { resolve = done; })); From 055a828122b91779296f02b86c4c13c7051d1ff6 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:00:47 +0900 Subject: [PATCH 04/19] test(Alden): ensure memory history does not skip reordered or malformed journal rows --- .../src/__tests__/collection-history.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/desktop/src/__tests__/collection-history.test.ts b/desktop/src/__tests__/collection-history.test.ts index 34bd3a8b..5be0dc62 100644 --- a/desktop/src/__tests__/collection-history.test.ts +++ b/desktop/src/__tests__/collection-history.test.ts @@ -109,6 +109,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() + .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() + .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() .mockResolvedValueOnce(response([event(20)], 20)) From 91371a47aea0f55ffc9071c7cce630763d50623a Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:01:51 +0900 Subject: [PATCH 05/19] test(Alden): preserve typed read args and event TTL during cursor retries --- desktop/src/__tests__/collection-activity.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/desktop/src/__tests__/collection-activity.test.ts b/desktop/src/__tests__/collection-activity.test.ts index e2b363a5..33fd04ec 100644 --- a/desktop/src/__tests__/collection-activity.test.ts +++ b/desktop/src/__tests__/collection-activity.test.ts @@ -75,12 +75,11 @@ describe('committed graph activity consumption', () => { 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 () => page(12, [event(11), event(12)])) + const read = vi.fn(async (_: Record) => 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); - vi.setSystemTime(12000); for (let attempt = 0; attempt < 3; attempt++) { await vi.advanceTimersByTimeAsync(2500); expect(consumer.diagnostics().cursor).toBe(10); From 7013ecaad6f3d4829c197c266faff2d81e8ce797 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:04:45 +0900 Subject: [PATCH 06/19] feat(Alden): verify current source/index lineage on expanded memory-history entries --- desktop/src/collection-history.ts | 69 ++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/desktop/src/collection-history.ts b/desktop/src/collection-history.ts index a55d63fd..b9d4394b 100644 --- a/desktop/src/collection-history.ts +++ b/desktop/src/collection-history.ts @@ -94,6 +94,7 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe let stream: string | null = null, aggregate: Counts | null = null; let historical = false, newCount = 0; const expanded = new Set(); + const activeReadbacks = new Map | null>>(); let timer: ReturnType | null = null; const listeners = new AbortController(); const virtual = new VirtualList(list as HTMLElement, row => String(row.sequence), row => { @@ -123,6 +124,8 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe if (typeof value === 'string' && value) identifiers.append(make('dt', '', String(label)), make('dd', '', value)); } more.append(identifiers); + const readback = make('p', 'collection-readback', ''); readback.hidden = true; + more.append(readback); if (typeof row.source_url === 'string') { try { const url = new URL(row.source_url); @@ -134,11 +137,75 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe } article.append(time, copy, badge, more); return article; }, 82); + async function verifyExpanded(detail: HTMLDetailsElement, sequence: number): Promise { + const row = items.find(entry => entry.sequence === sequence); + const projects = row?.projects; + if (!row || !['stored', 'indexed'].includes(String(row.stage)) + || !Number.isSafeInteger(sequence) || !Array.isArray(projects) + || !projects.length || projects.length > 16 + || !projects.every(project => typeof project === 'string' && project.length > 0 && project.length <= 128) + || !['document_id', 'version', 'target_id', 'run_id'].every(key => + typeof row[key] === 'string' && (row[key] as string).length > 0 && (row[key] as string).length <= 256)) return; + const output = detail.querySelector('.collection-readback'); + if (!output) return; + output.hidden = false; output.textContent = '현재 저장 상태를 다시 확인합니다.'; + const ticket = epoch; + const params = { projects, document_id: row.document_id, expected_version: row.version, target_id: row.target_id }; + const key = JSON.stringify(params); + let pending = activeReadbacks.get(key); + if (!pending) { + pending = load('collection-trace', { query: key }); + activeReadbacks.set(key, pending); + void pending.finally(() => { if (activeReadbacks.get(key) === pending) activeReadbacks.delete(key); }).catch(() => {}); + } + let proof: Record | null = null; + try { proof = await pending; } catch { /* No raw provider exception is shown in the UI. */ } + if (dead || !visible || page !== 'history' || ticket !== epoch || !detail.open + || !list.contains(detail) || detail.dataset.sequence !== String(sequence)) return; + if (proof?.state === 'version_changed') { + output.textContent = '후속 개정으로 현재 버전이 달라졌습니다. 이 기록의 당시 처리 단계는 보존됩니다.'; + return; + } + if (proof?.state === 'not_in_scope') { + output.textContent = '현재 권한에서 이 원본을 재확인할 수 없습니다.'; + return; + } + const sameScope = proof?.ok === true && ['available', 'removed'].includes(String(proof.state)) + && proof.document_id === row.document_id && proof.version === row.version + && proof.target_id === row.target_id && Array.isArray(proof.projects) + && proof.projects.length === projects.length + && proof.projects.every((project, index) => project === projects[index]); + if (!sameScope) { output.textContent = '저장 결과를 확인하지 못했습니다.'; return; } + const source = proof.source as Record | undefined; + const stages = proof.stages as Record | undefined; + const fts = proof.fts as Record | undefined; + const dense = proof.dense as Record | undefined; + const runMatches = proof.run_id === row.run_id; + if (!runMatches) { + output.textContent = '이후 처리 실행이 확인됐습니다. 현재 저장 상태가 이 이력의 결과를 대신하지 않습니다.'; + return; + } + const sourceVerified = source?.state === 'hash_verified'; + if (proof.state === 'removed') { + output.textContent = sourceVerified + ? '재조회 확인 · 원문 해시 보존 · 현재 그래프에서 제외됨' + : '원본 보존 상태를 다시 확인해야 합니다.'; + return; + } + const stored = sourceVerified && stages?.stored === true; + const indexed = stored && stages?.indexed === true && fts?.state === 'verified'; + const vector = dense?.state === 'stored_vector_binding_verified'; + output.textContent = (stored ? '저장·원문 해시 확인' : '저장 상태 재확인 필요') + + ' · FTS ' + (indexed ? '확인' : '미확인') + + ' · Dense ' + (vector ? '저장 바인딩 확인' : '미확인/대기') + + ' · 모델 실행은 별도 검증'; + } list.addEventListener('toggle', event => { const detail = event.target; if (!(detail instanceof HTMLDetailsElement) || !list.contains(detail)) return; const sequence = Number(detail.dataset.sequence); - if (detail.open) expanded.add(sequence); else expanded.delete(sequence); + if (detail.open) { expanded.add(sequence); void verifyExpanded(detail, sequence); } + else expanded.delete(sequence); }, { capture: true, signal: listeners.signal }); virtual.setVisible(false); const stopTimer = () => { if (timer) clearTimeout(timer); timer = null; }; From 7b33c3f452d2472aebf93b92e264f5f301af9037 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:05:27 +0900 Subject: [PATCH 07/19] feat(Alden): persist target-specific previous version in immutable storage history --- scripts/alden_collection.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/alden_collection.py b/scripts/alden_collection.py index 654169a1..332003f4 100644 --- a/scripts/alden_collection.py +++ b/scripts/alden_collection.py @@ -539,7 +539,14 @@ def ingest(self, target_id: str, records, *, cursor=None, origin="alden", (target_id, doc_id, version)) if complete_snapshot: db.execute('INSERT INTO snapshot_members VALUES(?)', (doc_id,)) - self._event(db, run_id, target_id, "stored", document_id=doc_id, version=version, change=category) + change_details = {"change": category} + # A changed version in this *target's own* membership + # has a concrete before/after lineage. A different + # target's version must never be passed off as its past. + if category == "revised" and membership and membership[0] != version: + change_details["previous_version"] = membership[0] + self._event(db, run_id, target_id, "stored", document_id=doc_id, + version=version, **change_details) db.execute('INSERT INTO search_updates VALUES(?,?,?) ON CONFLICT(document_id) DO UPDATE SET label=excluded.label,body=excluded.body', (doc_id,label,body)) self._event(db, run_id, target_id, "indexed", document_id=doc_id, version=version, index="sqlite_fts5", dense_index="not_yet_confirmed") From d661a78b9ce48304214bc5cb34c36c38bab59ef1 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:05:46 +0900 Subject: [PATCH 08/19] feat(Alden): expose previous version and fence lazy proof after journal replacement --- desktop/src/collection-history.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/desktop/src/collection-history.ts b/desktop/src/collection-history.ts index b9d4394b..3db693fb 100644 --- a/desktop/src/collection-history.ts +++ b/desktop/src/collection-history.ts @@ -120,7 +120,10 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe : String(details.reason ?? '원문 ID와 저장 버전을 연결한 기록입니다.'); more.append(make('p', '', explanation)); const identifiers = document.createElement('dl'); - for (const [label, value] of [['실행', row.run_id], ['원문 항목', row.document_id], ['저장 버전', row.version]]) { + const previousVersion = typeof details.previous_version === 'string' + && /^version:[0-9a-f]{64}$/.test(details.previous_version) ? details.previous_version : null; + for (const [label, value] of [['실행', row.run_id], ['원문 항목', row.document_id], + ['변경 전 버전', previousVersion], ['저장 버전', row.version]]) { if (typeof value === 'string' && value) identifiers.append(make('dt', '', String(label)), make('dd', '', value)); } more.append(identifiers); @@ -149,7 +152,7 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe const output = detail.querySelector('.collection-readback'); if (!output) return; output.hidden = false; output.textContent = '현재 저장 상태를 다시 확인합니다.'; - const ticket = epoch; + const ticket = epoch, currentStream = stream; const params = { projects, document_id: row.document_id, expected_version: row.version, target_id: row.target_id }; const key = JSON.stringify(params); let pending = activeReadbacks.get(key); @@ -160,8 +163,12 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe } let proof: Record | null = null; try { proof = await pending; } catch { /* No raw provider exception is shown in the UI. */ } - if (dead || !visible || page !== 'history' || ticket !== epoch || !detail.open - || !list.contains(detail) || detail.dataset.sequence !== String(sequence)) return; + const stillSelected = items.find(item => item.sequence === sequence); + if (dead || !visible || page !== 'history' || ticket !== epoch || stream !== currentStream || !detail.open + || !list.contains(detail) || detail.dataset.sequence !== String(sequence) + || !stillSelected || stillSelected.document_id !== row.document_id + || stillSelected.version !== row.version || stillSelected.target_id !== row.target_id + || stillSelected.run_id !== row.run_id) return; if (proof?.state === 'version_changed') { output.textContent = '후속 개정으로 현재 버전이 달라졌습니다. 이 기록의 당시 처리 단계는 보존됩니다.'; return; From c3ed764eb47a9f649149a2c72082d4bd80476e4a Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:06:17 +0900 Subject: [PATCH 09/19] test(Alden): preserve source-specific before/after versions without cross-target lineage --- tests/test_alden_collection.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_alden_collection.py b/tests/test_alden_collection.py index ab5456df..151e9ff1 100644 --- a/tests/test_alden_collection.py +++ b/tests/test_alden_collection.py @@ -146,6 +146,26 @@ def test_permissions_filter_graph_search_and_history(self): self.assertEqual(self.store.events(projects=["two"])["items"], []) self.assertEqual(len(self.store.search("검색", projects=["one"])), 1) + def test_revision_history_uses_previous_version_from_its_own_target_only(self): + primary=self.target() + secondary=self.target('channel-B','one') + self.store.ingest(primary,[self.record('shared',text='target A revision one')]) + before=self.store.graph_page(projects=['one'],target_id=primary)['nodes'][0]['source_version'] + self.store.ingest(secondary,[self.record('shared',text='target B different revision')]) + foreign=self.store.graph_page(projects=['one'],target_id=secondary)['nodes'][0]['source_version'] + self.assertNotEqual(before,foreign) + self.store.ingest(primary,[self.record('shared',text='target A revision two')]) + after=self.store.graph_page(projects=['one'],target_id=primary)['nodes'][0]['source_version'] + history=self.store.recent_events(projects=['one'],target_id=primary,stage='stored') + recent=next(row for row in history['items'] if row['version']==after) + self.assertEqual(recent['details']['change'],'revised') + self.assertEqual(recent['details']['previous_version'],before) + self.assertNotEqual(recent['details']['previous_version'],foreign) + secondary_history=self.store.recent_events(projects=['one'],target_id=secondary,stage='stored') + self.assertNotIn('previous_version',secondary_history['items'][0]['details']) + with self.store.database() as db: + self.assertGreaterEqual(db.execute('SELECT COUNT(*) FROM versions').fetchone()[0],3) + def test_busy_target_does_not_block_another_target(self): a, b = self.target(), self.target("channel-B") with self.store.target_lock(a): From c669bceced816802274f693534265701971df517 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:06:38 +0900 Subject: [PATCH 10/19] test(Alden): validate memory-history lazy source readback, identity and hidden response fencing --- .../src/__tests__/collection-history.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/desktop/src/__tests__/collection-history.test.ts b/desktop/src/__tests__/collection-history.test.ts index 5be0dc62..0a0896a5 100644 --- a/desktop/src/__tests__/collection-history.test.ts +++ b/desktop/src/__tests__/collection-history.test.ts @@ -157,6 +157,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().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('.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().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('.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) => void; + const load = vi.fn().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('.collection-event-detail')!; + detail.open = true; detail.dispatchEvent(new Event('toggle')); await vi.advanceTimersByTimeAsync(0); + const readback = detail.querySelector('.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().mockResolvedValue(response([])); setup(load); await vi.advanceTimersByTimeAsync(0); From 00c3c79d88bc0d388430f132084c0a15e203ad46 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:09:05 +0900 Subject: [PATCH 11/19] fix(Alden): narrow nullable evidence reply before reading verified fields --- desktop/src/collection-history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/collection-history.ts b/desktop/src/collection-history.ts index 3db693fb..0f279671 100644 --- a/desktop/src/collection-history.ts +++ b/desktop/src/collection-history.ts @@ -182,7 +182,7 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe && proof.target_id === row.target_id && Array.isArray(proof.projects) && proof.projects.length === projects.length && proof.projects.every((project, index) => project === projects[index]); - if (!sameScope) { output.textContent = '저장 결과를 확인하지 못했습니다.'; return; } + if (!sameScope || proof === null) { output.textContent = '저장 결과를 확인하지 못했습니다.'; return; } const source = proof.source as Record | undefined; const stages = proof.stages as Record | undefined; const fts = proof.fts as Record | undefined; From 5feb8ead4a6443453836e3111ace9c4ce9d3f7c8 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:10:33 +0900 Subject: [PATCH 12/19] fix(Alden): distinguish stored additions, revisions, removals and relation updates in memory history --- desktop/src/collection-history.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/desktop/src/collection-history.ts b/desktop/src/collection-history.ts index 0f279671..56e21fcb 100644 --- a/desktop/src/collection-history.ts +++ b/desktop/src/collection-history.ts @@ -7,6 +7,10 @@ const stages: Record = { discovered: '자료 발견', parsed: '내용 읽음', validated: '형식 확인', stored: '저장 완료', indexed: '검색 반영', failed: '실패', paused: '중지', }; +const storedChanges: Record = { + added: '새 자료 저장', revised: '기존 자료 수정', unchanged: '기존 자료 유지', + removed: '원본에서 제외', relations_changed: '관계 갱신', +}; const rows = (value: unknown): Row[] => Array.isArray(value) ? value.filter(v => v && typeof v === 'object' && !Array.isArray(v)) as Row[] : []; // The server delivers newest-first initial/older pages and oldest-first @@ -107,7 +111,9 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe copy.append(make('strong', '', String(row.target_label ?? '대상')), make('span', '', String(row.document_label ?? '처리 기록'))); const details = row.details && typeof row.details === 'object' ? row.details as Row : {}; const unchanged = details.change === 'unchanged'; - const label = unchanged && row.stage === 'stored' ? '기존 자료 유지' : stages[String(row.stage)] ?? '처리 기록'; + const label = row.stage === 'stored' + ? storedChanges[String(details.change)] ?? stages.stored + : stages[String(row.stage)] ?? '처리 기록'; const badge = make('span', 'collection-stage', label); badge.dataset.stage = String(row.stage); const more = document.createElement('details'); more.className = 'collection-event-detail'; more.dataset.sequence = String(row.sequence); more.open = expanded.has(Number(row.sequence)); @@ -296,7 +302,10 @@ export function wireCollectionHistory(load: typeof fetchSettingsAction = fetchSe totals.hidden = aggregate === null; if (aggregate) totals.textContent = '선택한 범위 · ' + aggregate.total.toLocaleString('ko-KR') + '개 단계 기록' + ' · 추가 ' + aggregate.changes.added.toLocaleString('ko-KR') + ' · 수정 ' + aggregate.changes.revised.toLocaleString('ko-KR') - + ' · 유지 ' + aggregate.changes.unchanged.toLocaleString('ko-KR') + ' · 실패 ' + aggregate.stages.failed.toLocaleString('ko-KR') + + ' · 유지 ' + aggregate.changes.unchanged.toLocaleString('ko-KR') + + ' · 제외 ' + aggregate.changes.removed.toLocaleString('ko-KR') + + ' · 관계 갱신 ' + aggregate.changes.relations_changed.toLocaleString('ko-KR') + + ' · 실패 ' + aggregate.stages.failed.toLocaleString('ko-KR') + ' · 중지 ' + aggregate.stages.paused.toLocaleString('ko-KR'); status.textContent = result.state === 'not_configured' ? '아직 수집 대상을 연결하지 않았습니다.' : newCount ? '새 처리 이력 ' + newCount + '개 · 최신 이력 보기로 돌아갈 수 있습니다.' From 1593f429ab10cd85cbfadd98c9dcd7f68873f04d Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:10:50 +0900 Subject: [PATCH 13/19] test(Alden): show accurate stage ledger counts and avoid unnecessary proof reads --- desktop/src/__tests__/collection-history.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/desktop/src/__tests__/collection-history.test.ts b/desktop/src/__tests__/collection-history.test.ts index 0a0896a5..ce3411da 100644 --- a/desktop/src/__tests__/collection-history.test.ts +++ b/desktop/src/__tests__/collection-history.test.ts @@ -50,6 +50,19 @@ describe('durable collection history', () => { expect(document.querySelector('.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().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().mockResolvedValue(response([ event(3, { stage: 'validated', source_url: 'javascript:alert(1)' }), From 7da0e35e895ff74b7d34e8a195e85d9e36c84860 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:12:02 +0900 Subject: [PATCH 14/19] docs(Alden): record durable activity cursor guards and scoped memory readback contract --- .../alden-activity-readback-20261011.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/architecture/alden-activity-readback-20261011.md diff --git a/docs/architecture/alden-activity-readback-20261011.md b/docs/architecture/alden-activity-readback-20261011.md new file mode 100644 index 00000000..140b5339 --- /dev/null +++ b/docs/architecture/alden-activity-readback-20261011.md @@ -0,0 +1,86 @@ +# 2026-10-11 — 수집 저널 커서, 현재 근거 재조회와 과거 버전 보존 + +기준점: PR #29의 커밋 `69cc1c4fe69318067c0aff81bf24dce268ebb959`. +새 변경은 `codex/alden-activity-order-20261011`에 별도 누적한다. +PR #27/#28/#29, 원래 Mac 복제본, 운영 DB, 모델, 설정·전송 큐 및 +현재 프로세스는 이 GitHub 소스 작업에서 수정하지 않는다. + +## 구현된 데이터 계약 + +```mermaid +flowchart LR + A[허용된 외부 자료] --> B[CollectionStore 대상별 저장] + B --> C[(원문 해시·버전·영속 runs/events)] + C --> D[저장·FTS5·Dense 파생 색인] + C --> E[순서 검증된 이벤트 페이지] + E --> F[기억 정리 UI] + E --> G[실제 3D 노드와 활동] + F -->|선택한 기록만| H[읽기 전용 collection-trace] + H --> D + H --> C +``` + +1. `desktop/src/knowledge/collection-activity.ts`: 현재 스트림 ID와 + 커서를 기준으로 허용된 200개 이하의 이벤트 페이지를 전체 검증한다. + 이벤트 시퀀스 역전·중복 ID·잘못된 타입·유효한 커서보다 뒤의 이벤트는 + **한 항목도 적용하지 않고** 현재 커서로 재조회한다. 검증 후에만 + `seen`, `pending`, `committed` 상태를 갱신한다. + 사용자가 화면을 숨겼거나 이벤트가 10초 표시 TTL을 넘기면 + 과거 이벤트를 방금 저장된 지식처럼 점등하지 않는다. +2. `desktop/src/collection-history.ts`: 첫 페이지와 과거 페이지는 + `DESC`, 증분 페이지는 `ASC` 순서를 요구한다. 잘못된 행이 섞이거나 + 순서가 뒤집히면 기존 목록·집계·커서가 그대로 남고, 같은 범위를 + 다시 읽는다. 복구할 수 없는 페이지를 임의 삭제/영구 처리했다고 + 표시하지 않는다. `revised`·`added`·`unchanged`·`removed`· + `relations_changed`를 구분하고 요약에서 제외/관계 변경도 센다. +3. `scripts/alden_collection.py`: `stored` 개정 이벤트에 같은 수집 + 대상의 `previous_version`을 연결한다. 같은 이름/원본 ID가 다른 + 대상에도 있더라도 그 대상의 버전을 변경 전 근거로 사용하지 않는다. + 기존 `run_id`, event ID, 영속 카운트·체크포인트 계약은 유지한다. +4. 사용자가 이력 상세를 여는 경우에만 `collection-trace`로 정확한 + `projects`·`document_id`·`target_id`·`expected_version`을 + 전달한다. 결과의 프로젝트·대상·버전·실행 ID가 모두 일치하면 + 원본 SHA-256 확인, 저장 확정, FTS, Dense 저장 벡터 바인딩을 + 각각 표시한다. 모델이 실제 추론에 사용됐거나 화면이 실제 GPU로 + 렌더링됐다고 주장하지 않는다. 후속 개정, 권한 변경, 원본 제외, + 조회 실패를 구별한다. 상세 조회는 새 수집 이벤트를 만들지 않는다. +5. 화면 숨김·범위 변경·저널 교체·늦게 도착한 다른 기록의 결과를 + 현재 상세에 반영하지 않는다. 동시에 같은 원본을 펼친 요청은 + 진행 중인 하나의 읽기를 공유하며, 완료 뒤 캐시를 영구 고정하지 + 않아 다음 열람 시 현재 버전을 다시 확인할 수 있다. + +## 검사 범위 + +- 초기 UI 검증 [GitHub Actions 38070079466](https://github.com/twoimo/alden/actions/runs/38070079466): + 활동/이력 역전 복구까지 macOS UI **467개 통과** 및 TypeScript/Vite 빌드 성공. + 앞선 실패 실행은 표시 TTL을 넘긴 테스트 시계의 오류로 구분하며 + 실제 실패 로그를 지우지 않았다. +- 상세 근거와 이전 버전 추가 후 + [GitHub Actions 38070565014](https://github.com/twoimo/alden/actions/runs/38070565014): + Python 60개, UI 470개, 타입·번들 빌드 통과. 최초 재검사에서 + TypeScript nullable guard 미비로 빌드 실패한 로그는 보존하고 + null 명시 검사 후 성공한 결과를 별도로 연결했다. +- 최신 표시 및 집계 검증은 코드 해시가 고정된 CI 실행에 + 연결한 뒤 결과를 기록한다. UI DOM 검사와 사용자의 Mac 실물 + 화면/GPU/입력·전체 설치본은 독립 검증이다. +- 자체 MCP `alden_knowledge_trace`의 저장 근거 경로는 + [이전 실제 stdio 테스트](alden-pipeline-readback-20261011.md)에서 + 확인했다. 외부 `osk-system` 서버를 실제 조회한 증거가 + 아니며, 동일 구분을 유지한다. + +## 배포 및 복구 조건 + +작업 Mac의 `goal-objective.md`와 기존 `outputs/STATUS.md`는 +Core/Codex 도구에 필요한 유효한 `turn_token`이 현재 대화에 +제공되지 않았고 Desktop Computer Use 연결도 노출되지 않아 +원문을 직접 열거나 원격 소스를 그 위치에 덮어쓸 수 없었다. +PR #27 → PR #28 → PR #29 → 이 브랜치의 변경 계보를 +확인하되 원본 복제본과 미커밋 편집을 실제 비교하기 전에는 +병합·설치·서비스 재시작을 실행하지 않는다. + +기존 unsigned 0.3.58 arm64 앱 검증 빌드의 커밋 +`c494a53c5ab649f49239e819fa1cc3ba13c6b9fd`는 +**이번 새 이력 소비 및 재조회 변경 전**의 버전이다. +최신 코드를 반영한 설치본·실물 사용 확인과 +Developer ID 서명·공증, 운영 MCP 호스트별 검증, 물리 음성/MLX +로드와 자원 지연은 여전히 미완료다. From 4a36eff13c80c28a40a5cac6ec973e229d266cc1 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:13:05 +0900 Subject: [PATCH 15/19] docs(Alden): record ordered memory events, lazy proof readback and macOS CI --- docs/architecture/alden-activity-readback-20261011.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/architecture/alden-activity-readback-20261011.md b/docs/architecture/alden-activity-readback-20261011.md index 140b5339..6125a239 100644 --- a/docs/architecture/alden-activity-readback-20261011.md +++ b/docs/architecture/alden-activity-readback-20261011.md @@ -60,9 +60,12 @@ flowchart LR Python 60개, UI 470개, 타입·번들 빌드 통과. 최초 재검사에서 TypeScript nullable guard 미비로 빌드 실패한 로그는 보존하고 null 명시 검사 후 성공한 결과를 별도로 연결했다. -- 최신 표시 및 집계 검증은 코드 해시가 고정된 CI 실행에 - 연결한 뒤 결과를 기록한다. UI DOM 검사와 사용자의 Mac 실물 - 화면/GPU/입력·전체 설치본은 독립 검증이다. +- 최종 [macOS CI 38070713835](https://github.com/twoimo/alden/actions/runs/38070713835): + 정확한 검증 브랜치 SHA `6250f436ccaa7d41b0f1a1cebf269d0967e50266`. + 저장·스냅샷·근거 경로 Python **60/60**, UI 전체 **471/471**, + TypeScript/Vite 프로덕션 빌드 **성공**. + 검증된 소스/테스트 blob SHA는 독립 구현 브랜치와 동일해야 한다. + 사용자의 Mac 실물 화면/GPU/입력·전체 설치본은 독립 검증이다. - 자체 MCP `alden_knowledge_trace`의 저장 근거 경로는 [이전 실제 stdio 테스트](alden-pipeline-readback-20261011.md)에서 확인했다. 외부 `osk-system` 서버를 실제 조회한 증거가 From 297e54ad2f4e94613b8ec780c6278e69283af75e Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:13:10 +0900 Subject: [PATCH 16/19] docs(Alden): record ordered memory events, lazy proof readback and macOS CI --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index ec4abc38..a14d4b9b 100644 --- a/README.md +++ b/README.md @@ -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). From 7b280a93334779518ed81bb76423d3da769c7ad3 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:13:14 +0900 Subject: [PATCH 17/19] docs(Alden): record ordered memory events, lazy proof readback and macOS CI --- desktop/DESIGN.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/desktop/DESIGN.md b/desktop/DESIGN.md index ec0eebcd..2b2d291f 100644 --- a/desktop/DESIGN.md +++ b/desktop/DESIGN.md @@ -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 From 7c4abb9d77d141fd2440ef834e7fbf1207237da4 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:13:18 +0900 Subject: [PATCH 18/19] docs(Alden): record ordered memory events, lazy proof readback and macOS CI --- docs/ALDEN_CONTINUATION.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ALDEN_CONTINUATION.md b/docs/ALDEN_CONTINUATION.md index 97602992..aab32dd4 100644 --- a/docs/ALDEN_CONTINUATION.md +++ b/docs/ALDEN_CONTINUATION.md @@ -1,3 +1,8 @@ +> 2026-10-11 신규: [영속 수집 이벤트 순서·재연결 보호, 변경 전후 버전, 기억 정리 원본 재조회](architecture/alden-activity-readback-20261011.md). +> 분리 브랜치 `codex/alden-activity-order-20261011`; macOS CI `38070713835`: Python 60개, UI 471개, TS/Vite 빌드 통과. +> **구현됨 / GitHub macOS 실제 실행 검증됨**, 소유자 Mac의 설치·usk-system/osk-system·실제 음성·서명·공증은 미검증. +> 기존 `outputs/STATUS.md`는 Mac-native Core의 유효한 turn_token 부재로 직접 열거나 갱신할 수 없었다. 원본 프로젝트·미커밋 변경·운영 데이터·전송 큐를 덮어쓰지 않았다. + > 2026-10-11 다중 대상 검색 추가: 같은 프로젝트 안의 대상별 버전·후보·관계 근거를 `target_id`로 격리하고, 최신성 반복문의 범위 변수 충돌을 수정했다. [macOS CI 38068930145](https://github.com/twoimo/alden/actions/runs/38068930145): Python 114/114, UI 30/30, TS/Vite 성공. 운영 대상 검색/모델/호스트 실물 E2E는 아직 확인하지 못했다. > 2026-10-11 추가 통합: [스냅샷 A→삭제→A 복원 및 수집→색인→이력→그래프/MCP 일치 검사](architecture/alden-pipeline-readback-20261011.md). From c62d6b7763a6644151aa18ff3a4edc7f239e3821 Mon Sep 17 00:00:00 2001 From: Yeonwoo Choi <32544727+twoimo@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:13:32 +0900 Subject: [PATCH 19/19] docs(Alden): clarify external osk-system validation gate --- docs/ALDEN_CONTINUATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ALDEN_CONTINUATION.md b/docs/ALDEN_CONTINUATION.md index aab32dd4..a9092e2c 100644 --- a/docs/ALDEN_CONTINUATION.md +++ b/docs/ALDEN_CONTINUATION.md @@ -1,6 +1,6 @@ > 2026-10-11 신규: [영속 수집 이벤트 순서·재연결 보호, 변경 전후 버전, 기억 정리 원본 재조회](architecture/alden-activity-readback-20261011.md). > 분리 브랜치 `codex/alden-activity-order-20261011`; macOS CI `38070713835`: Python 60개, UI 471개, TS/Vite 빌드 통과. -> **구현됨 / GitHub macOS 실제 실행 검증됨**, 소유자 Mac의 설치·usk-system/osk-system·실제 음성·서명·공증은 미검증. +> **구현됨 / GitHub macOS 실제 실행 검증됨**, 소유자 Mac의 설치·osk-system·실제 음성·서명·공증은 미검증. > 기존 `outputs/STATUS.md`는 Mac-native Core의 유효한 turn_token 부재로 직접 열거나 갱신할 수 없었다. 원본 프로젝트·미커밋 변경·운영 데이터·전송 큐를 덮어쓰지 않았다. > 2026-10-11 다중 대상 검색 추가: 같은 프로젝트 안의 대상별 버전·후보·관계 근거를 `target_id`로 격리하고, 최신성 반복문의 범위 변수 충돌을 수정했다. [macOS CI 38068930145](https://github.com/twoimo/alden/actions/runs/38068930145): Python 114/114, UI 30/30, TS/Vite 성공. 운영 대상 검색/모델/호스트 실물 E2E는 아직 확인하지 못했다.