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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ for full commit body + generated notes.

## Unreleased

- **Export carried the time window, not the filter.** The HTTP view built its
export subset from the oldest and newest *loaded* row's timestamps, so method,
status and text predicates were dropped on the way out: unrelated traffic
inside that window went into the artifact, and matches past the loaded page
did not. Export now carries the investigation's actual conditions and
re-reads them against the recorded events — every page, not the one on
screen — and names those conditions in the preview before anything is
written. Controls that only change presentation (sorting, collapsing, lane
visibility, highlighting) are no longer mistaken for a selection, and a
bundle that carries less than the whole chain says so in its manifest.

- **The causal links were being computed and then thrown away.** The backend
has returned a finding's whole causal component — events *and* the edges
between them — since v0.6.89.5. The renderer kept the ids for dimming the
Expand Down
45 changes: 45 additions & 0 deletions e2e/filtered-export.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { test, expect } from '@playwright/test'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { launchWithTempHome, openTestProject, openView } from './helpers'

for (const locale of ['en', 'zh-TW']) {
test(`filtered export is explicit, frozen and readable in ${locale}`, async () => {
const { app, page, tmpHome } = await launchWithTempHome(process.env.REDLOG_PACKAGED_APP)
try {
await page.evaluate(l => localStorage.setItem('redlog-locale',l), locale)
await openTestProject(page,'filtered-export')
const base=`http://127.0.0.1:${readFileSync(join(tmpHome,'.redlog','api-port'),'utf8').trim()}`
const token=readFileSync(join(tmpHome,'.redlog','api-token'),'utf8').trim()
for (const command of ['needle evidence', 'other evidence']) {
const result=await fetch(base+'/api/events/seed',{method:'POST',headers:{'content-type':'application/json',authorization:`Bearer ${token}`},body:JSON.stringify({agent_type:'shell',target_id:'example.test',data:{subtype:'command_end',command,exit_code:0}})})
expect(result.ok).toBe(true)
}
await openView(page,'search')
await page.getByTestId('search-input').fill('needle')
await expect(page.getByText('needle evidence',{exact:false}).first()).toBeVisible()
const win=await app.browserWindow(page); await win.evaluate(w=>w.setSize(800,850))
const current=locale==='en'?'Current selection':'目前篩選結果'
const exportLabel=locale==='en'?'Export':'匯出'
await page.getByLabel(exportLabel,{exact:true}).click()
await page.getByRole('button',{name:current,exact:false}).click()
await expect(page.getByTestId('export-preview-subset')).toContainText('needle')
await expect(page.getByTestId('export-projection-notice')).toBeVisible()
await expect(page.getByTestId('export-confirm')).toBeEnabled()
const dialog=page.getByTestId('export-dialog')
expect(await dialog.evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true)
await page.getByTestId('export-confirm').focus(); await page.keyboard.press('Enter')
await expect(dialog).toHaveCount(0)
// A legitimate empty selection still previews; it cannot be confirmed.
await page.getByTestId('search-input').fill('never-recorded-token')
await page.getByLabel(exportLabel,{exact:true}).click()
await page.getByRole('button',{name:current,exact:false}).click()
await expect(page.getByTestId('export-confirm')).toBeDisabled()
await page.keyboard.press('Escape'); await page.keyboard.press('Escape')
await page.getByTestId('search-input').fill('session:')
await page.getByLabel(exportLabel,{exact:true}).click()
await page.getByRole('button',{name:current,exact:false}).click()
await expect(page.getByText('Invalid export query',{exact:false})).toBeVisible()
} finally { await app.close() }
})
}
39 changes: 39 additions & 0 deletions specs/056-filtered-evidence-export/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Specification Quality Checklist: Filtered evidence export

**Purpose**: Validate requirements before technical planning.
**Created**: 2026-09-28
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details prescribe frameworks, modules or algorithms.
- [x] Focused on the operator's handoff and evidence integrity.
- [x] Written around observable user journeys.
- [x] Mandatory sections completed.

## Requirement Completeness

- [x] No unresolved user clarification markers.
- [x] Requirements have observable acceptance criteria.
- [x] Success criteria are measurable.
- [x] Success criteria describe user-visible results rather than implementation.
- [x] Acceptance scenarios cover each story.
- [x] Edge cases include failure, empty selections, pair boundaries and data changes.
- [x] Scope excludes capture engines, report editors and compatibility work.
- [x] Dependencies and reasonable assumptions are explicit.

## Feature Readiness

- [x] Functional requirements map to the three stories and five success criteria.
- [x] Primary flows include both whole-project and current-selection export.
- [x] Outcomes can be verified with deterministic populations and artifact inspection.
- [x] Technical approach remains a planning decision, particularly bundle verification.

## Review notes

Requirements review only; this does not claim implementation or runtime success.
FR-007 makes full-chain versus projection semantics a prerequisite for enabling
filtered bundles. FR-010 and Assumptions retain Timeline highlighting as a
presentation aid instead of changing its existing meaning implicitly.
No extension hooks or spec-template overrides are configured. Ready for
question-free Clarify and technical Plan; Analyze runs after Tasks exist.
9 changes: 9 additions & 0 deletions specs/056-filtered-evidence-export/contracts/selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# IPC and delivery contract

resolveExportPlan accepts a declarative ExportRequest; rejects unknown subset kinds, malformed filters/query/time/HTTP predicates and unsupported policy. Returns either explicit error or frozen preview, including zero matches. executeExportPlan accepts only planId and checks source/policy/attachments.

queryHttpFlowPage accepts HTTP filters in addition to EventFilter; both screen and export use it. Filtering precedes page limits. Snapshot is injected by export; selected pair members must remain below the snapshot even when outside selected request-start time.

UI offers whole project and current selection independently. Selection summaries include query, time, target, type, scope, personal, tier and HTTP predicates. Sorting/highlighting/collapse never silently change membership. Preview states projection limits; zero is visible and confirmation disabled. All strings English/Traditional Chinese.

Verifier projection success explicitly means delivered files and eligible original rows verified, not source chain completeness or transformed source bytes verified. Unknown modes, missing/modified listed files, duplicate IDs, count mismatch and incorrect original row hashes fail.
7 changes: 7 additions & 0 deletions specs/056-filtered-evidence-export/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Selection model

ExportSubset = all | time-range | selection. Selection contains projection events/http, EventFilter (target/type/time/scope/personal/tier), optional query text, HTTP predicates and optional excludeHousekeeping. Renderer cannot supply scope/private-domain policy. All predicates validated; nested objects cloned/frozen.

Snapshot contains rowid boundary per tier. Plan contains selected IDs/digest and exact counts; optional included exchange count differs from event count. The request itself is shown in preview and retained in manifest. Changing UI does not mutate a plan.

Bundle evidence metadata: kind complete-chain/projection, transformed event IDs, source boundary separately from delivered per-tier counts. No new database tables. Projection has original IDs/hash/prev_hash/signature; no synthesized chain. Whole casts are disclosed and may be excluded individually.
23 changes: 23 additions & 0 deletions specs/056-filtered-evidence-export/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Plan: complete filtered evidence export

Branch: feat/056-filtered-evidence-export · 2026-09-28 · [Spec](spec.md)

## Summary
Carry declarative investigation predicates through existing ExportPlan; resolve over SQLite before pagination, snapshot identities, execute frozen selection. HTTP pages and export call the same flow query. Bundles explicitly distinguish a complete source chain from a projection.

## Technical Context
TypeScript, React, Electron, better-sqlite3; existing Vitest/Playwright and Python verifier. No new dependencies, schema migration or capture engine. Desktop macOS/Windows/Linux. Complete selection may be large; SQL filters precede limits, use keyset pages for resolving ordinary event queries. Keep all source IDs and bytes unless an explicit existing privacy policy transforms delivery.

## Constitution Check
Evidence integrity: preserve source hashes, mark transformed projections. Surface truthfulness: exact event/exchange counts, no loaded-page inference. Canonical semantics: use event query and HTTP flow query. Query completeness: iterate hasMore under snapshot. Preview/execute: freeze IDs/policy/attachments. Explicit failure: reject invalid input/unsupported combinations. Architectural restraint: existing modules only. PASS before and after design; no exceptions.

## Project Structure
- src/core/export-plan.ts: validated selection contract
- src/core/db/event-queries.ts: snapshot-aware canonical event/HTTP query
- src/main/ipc/data-export.ts: selection resolution and approved execution
- src/core/bundle-export.ts, tools/redlog-verify.py: projection contract
- src/renderer/src/components/{ExportMenu,HttpHistoryPanel,Timeline,Search,TranscriptView}.tsx and lib/exportScope.ts: current-selection contribution
- test/: unit/integration regression; e2e/: real operator journeys

## Implementation sequence
US1 event selection → US2 HTTP flow selection → US3 projection verification → cross-layer/UI verification. Tests precede implementation. See tasks.md. Installer acceptance is recorded separately from feature correctness.
11 changes: 11 additions & 0 deletions specs/056-filtered-evidence-export/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Validation

Use repository dependencies. npm test (Node SQLite); npm run build then npm run e2e (Electron SQLite). Do not rebuild both ABIs concurrently.

Seed matching events beyond first page, interleaved nonmatches, both tiers, same timestamps and mixed target casing. Compare actual JSON/NDJSON/Timeline/HAR/bundle contents to expected identities. Preview then append records/change attachments: append stays out, changed approved evidence fails. Empty and invalid queries remain distinct.

HTTP: method+status+host+text filters, request in interval/response out, response-only, incompatible type and chained-only. Page before and after Load more has identical export membership.

UI: current/whole selection, error/retry, zero, keyboard confirmation, English/Traditional Chinese, compact desktop. Python verifier tests complete/projection/tamper/missing file/count/duplicate IDs.

Installation validation is documented separately; available binaries/resources are not installation success.
8 changes: 8 additions & 0 deletions specs/056-filtered-evidence-export/research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Research decisions

1. Event selection: reuse executeEventQuery/queryEventsPage with snapshot rowid fences and trusted scope loaded in main; do not accept renderer-supplied scope policy. Empty typed query is an ordinary filtered read; invalid recognized syntax is an error. Alternatives rejected: loaded IDs or timestamp envelope.
2. HTTP: filter grouped request/response exchanges before page limits, by shared target/time/scope/tier and method/status-prefix/host/text. Time uses request start or earliest surviving row. Repeated records select earliest request and latest response, with rowid tie-breaks. Missing/empty/non-string flow IDs are not exchanges. Export preserves recorded rows of selected exchanges; HAR reports response-only exchange without inventing a request record.
3. Bundle: implement projection mode, not silent relaxation. File digests/counts/IDs and untransformed row hashes remain checked; transformed rows are declared. Source chain boundary is metadata, never a claim that omitted links were verified. Complete unchanged bundles retain strict contiguous-chain verification. Reject unknown evidence kinds.
4. Environment: local macOS ARM, no running Windows VM, stale VirtualBox executable wrapper, no native wsl/pwsh. Implement portable installation checks and record actual executed platforms; do not claim Windows/WSL verification based on resource checks.

Question-free Clarify: FR007 permits unsupported formats to refuse, but US3 explicitly requires filtered bundle semantics; implement the projection contract. No user preference is needed. Existing domain docs remain canonical and receive explicit updates.
Loading
Loading