Skip to content

feat(lifecycle): add durable artifact-deletion receipt ledger - #277

Closed
seonghobae wants to merge 34 commits into
fix/admin-endpoint-auth-cleanfrom
feat/durable-deletion-receipt-ledger
Closed

feat(lifecycle): add durable artifact-deletion receipt ledger#277
seonghobae wants to merge 34 commits into
fix/admin-endpoint-auth-cleanfrom
feat/durable-deletion-receipt-ledger

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Buyer-visible lifecycle gap

Authorized tenant deletion currently removes metadata before best-effort artifact-store deletion. Storage failure can therefore leave confidential bytes without restart-replayable cleanup evidence. Issue #263 owns the complete transactional tombstone, outbox, revocation, cleanup-worker, and truthful API/UI solution.

This Draft is the bounded receipt-ledger foundation only. It does not claim that artifact cleanup or deletion completion is integrated.

Exact current stack

Exact head: 8448fdd98d4a6c58f4e5407fcfcce903785abaa8.

The branch was reconstructed directly on parent PR #268 exact head d57f1aa8153aa092607a147c9587c683f009ea06 after that parent advanced. Fresh comparison reports:

  • merge base equals the exact parent head;
  • 2 commits ahead and 0 commits behind;
  • exactly 11 changed files;
  • the parent’s fail-closed null/blank tenant-scoped content-lookup regression is inherited unchanged;
  • every predecessor-head workflow result and review is stale.

The test-first sequence is:

  1. RED 7e0c8f3da1f6d5063b6172da2c40b5b5ce41eb32 — defines deterministic receipt identity, replay, crash-tail, transition, failure, and coverage contracts.
  2. GREEN 8448fdd98d4a6c58f4e5407fcfcce903785abaa8 — adds the standalone and file-backed ledger implementation, configuration, documentation, and changelog evidence.

Changed ledger files:

  • CHANGELOG.md
  • docs/security/2026-08-06-durable-artifact-deletion-receipts.md
  • src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java
  • src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java
  • src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java
  • src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java
  • src/main/resources/application.yml
  • src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerCoverageTest.java
  • src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerCrashTailTest.java
  • src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerReplayValidationTest.java
  • src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java

No endpoint, controller, existing repository mutation, artifact-store mutation, scheduler, workflow, dependency, packaging, version, or release is changed.

Receipt and MSA contract

ArtifactDeletionReceiptStore is the standalone and replaceable service boundary. Its reference adapter supports direct in-memory construction and Spring file-backed operation at data/artifact-deletion-receipts.log.

Each immutable RECEIPT_V1 identity binds deletion request ID, tenant, permanently reserved conversion-job ID, exact artifact SHA-256, privacy-safe audit correlation, and request time. Exact duplicates are idempotent. Any same-job identity conflict fails closed.

The monotonic lifecycle is:

DELETION_REQUESTED → METADATA_TOMBSTONED → ARTIFACT_CLEANUP_PENDING → ARTIFACT_CLEANUP_COMPLETED

with retry:

ARTIFACT_CLEANUP_PENDING → ARTIFACT_CLEANUP_FAILED → ARTIFACT_CLEANUP_PENDING.

Completion is terminal. Time cannot reverse. Failure evidence is restricted to controlled [a-z0-9_]{1,64} codes.

Durability and replay boundary

  • Every strict UTF-8 RECEIPT_V1 record is bounded to 16 KiB.
  • Append uses a fixed host-independent LF commit delimiter and returns durable state only after FileChannel.force(true).
  • Every non-empty unterminated EOF tail fails closed and remains intact for controlled, reviewable recovery; startup never silently truncates forensic evidence.
  • Replay rejects malformed Base64URL, missing or blank required fields, invalid timestamp/state/count values, immutable-identity conflict, non-monotonic time, illegal successor states, unsafe failure details, and oversized input.
  • Attempt count and attempt time must coexist consistently. Requested and tombstoned states cannot contain attempt evidence. Retry-pending and completion preserve prior attempt evidence, and only pending-to-failed may increment the count.

Remaining product work

This PR does not resolve #268's incomplete-cleanup finding. Still required under #263:

  1. atomic request, metadata tombstone, and outbox task;
  2. tenant- and exact-generation-authorized cleanup worker;
  3. signed-link revocation before completion;
  4. durable retry, backoff, dead-letter, and operator recovery;
  5. truthful 202 and status semantics;
  6. accessible deletion-progress and failure UI;
  7. real PDF/storage/process-restart integration evidence.

Exact-head acceptance state

For exact current head 8448fdd98d4a6c58f4e5407fcfcce903785abaa8:

  • exact-head fuzz run 31096007229 completed successfully across all three targets;
  • CodeRabbit commit status is successful, but no formal exact-current-head review submission exists;
  • CI, Security Scan, SAST, Strix, merge-compatible full Maven/JaCoCo/Javadoc evidence, OpenCode/Noema, and counted independent write-authorized approval are absent and therefore not passing;
  • predecessor-head fuzz and review results are not reused;
  • the incomplete-cleanup thread on parent fix(security): require signed tenant claims on admin endpoints #268 remains valid and unresolved.

Keep this PR Draft. Parent #270 must integrate before #268, and #268 must integrate before this slice. After reconciliation onto protected main, rerun every exact-head and base-sensitive gate and merge only with zero actionable unresolved threads and all repository protections satisfied. Do not weaken tests, infer approval from advisory status, resolve the cleanup finding prematurely, bypass protections, or publish a release.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: af80496e-a8b1-4d39-afa6-fe81f6afa141

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

불변 artifact 삭제 영수증 모델과 저장소 계약을 추가했습니다. 메모리 및 restart-replay 파일 ledger가 상태 전이, 멱등성, 실패 재시도, strict UTF-8 영속화를 처리합니다. 실제 삭제와 원자적 tombstone 통합은 포함하지 않습니다.

Changes

Artifact deletion receipts

Layer / File(s) Summary
영수증 계약과 lifecycle 상태
src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java, ArtifactDeletionState.java, ArtifactDeletionReceiptStore.java, docs/security/...
불변 식별자, 상태, 시각, 재시도 및 제한된 실패 코드를 정의했습니다. 허용된 단조 상태 전이와 저장소 API를 추가했습니다.
Ledger 요청과 상태 전이
src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java, src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java
멱등 요청, 식별자 충돌 거부, tombstone·cleanup 전이, 실패 재시도, pending 조회와 완료 제외를 구현하고 테스트했습니다.
파일 영속화와 restart replay
src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java, src/main/resources/application.yml, docs/security/..., CHANGELOG.md, src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java
16 KiB bounded strict UTF-8 레코드, Base64URL 필드, FileChannel.force(true), 손상 레코드 거부와 재시작 복원을 추가했습니다. 기본 ledger 경로와 관련 보안 문서를 추가했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ArtifactDeletionLedger
  participant ArtifactDeletionReceipt
  participant FileChannel
  Caller->>ArtifactDeletionLedger: 요청 또는 상태 전이
  ArtifactDeletionLedger->>ArtifactDeletionReceipt: 식별자와 다음 상태 검증
  ArtifactDeletionReceipt-->>ArtifactDeletionLedger: 새 영수증 snapshot 반환
  ArtifactDeletionLedger->>FileChannel: UTF-8 레코드 append 및 force(true)
  FileChannel-->>ArtifactDeletionLedger: durable write 결과 반환
Loading

Possibly related issues

  • 이슈 263: durable deletion receipt, 상태 머신, 재시도 및 restart recovery 목표를 이 변경이 구현합니다.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 영수증 원장 요구사항은 구현했지만, 잘못된 시도 증거를 허용하는 재생 검증 결함이 [#268] 요구사항을 충족하지 못합니다. 재생 검증에서 시도 횟수와 lastAttemptAt의 조합, 상태별 시도 증거, 현재 상태 시각과의 순서를 fail closed로 검증하고 회귀 테스트를 추가하십시오.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed 변경 사항은 영수증 계약, 원장 구현, 설정, 문서 및 관련 테스트 범위에 있으며 확인된 무관한 코드 변경은 없습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 영속 아티팩트 삭제 영수증 원장의 추가라는 변경사항을 정확하고 간결하게 설명합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/durable-deletion-receipt-ledger

Comment @coderabbitai help to get the list of available commands.

@seonghobae
seonghobae force-pushed the feat/durable-deletion-receipt-ledger branch from cdb3465 to 2f6bc8d Compare August 6, 2026 09:59

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Independently review exact current head 2f6bc8ddada68d73c7b35641d005a7fe02ee9c0a relative to parent #268 exact 3e98e6fbf8af006f08e50463974468e89daf168d. Treat the pre-reconstruction branch history and every predecessor result as stale.

Verify the eight-file Slice B foundation only: ArtifactDeletionReceiptStore modular contract; immutable request/tenant/job/SHA-256/audit-correlation identity; exact duplicate object idempotence; fail-closed same-job conflicts; monotonic state and timestamps; terminal completion; controlled privacy-safe failure codes; strict bounded UTF-8 RECEIPT_V1 serialization; append-only forced storage; restart replay; legal transition validation; completed receipt retention and pending exclusion; default file path versus standalone in-memory construction; complete public Javadocs; APA 7th ADR references; and preservation of #268's latest missing-tenant content-hash hardening.

Confirm there is no endpoint/controller/repository-delete/artifact-delete/worker/scheduler/workflow/dependency/release change and that the PR does not claim to resolve the still-open incomplete-cleanup finding. Report exact-head actionable findings or a no-blocker advisory conclusion after independent inspection. Keep Draft and do not infer counted independent approval.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='2f6bc8ddada68d73c7b35641d005a7fe02ee9c0a'
base='3e98e6fbf8af006f08e50463974468e89daf168d'

printf '%s\n' '== Revision verification =='
git rev-parse HEAD
git cat-file -t "$head"
git cat-file -t "$base"
git merge-base "$head" "$base"
git rev-list --left-right --count "$base...$head"

printf '%s\n' '== Parent-relative changed paths and status =='
git diff --name-status "$base" "$head"
git diff --stat "$base" "$head"

printf '%s\n' '== Java source/test outlines =='
for f in \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java \
  src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java
do
  if [ -f "$f" ]; then
    echo "-- $f"
    ast-grep outline "$f" --items all || true
  fi
done

printf '%s\n' '== Relevant changed file contents =='
for f in \
  CHANGELOG.md \
  docs/security/2026-08-06-durable-artifact-deletion-receipts.md \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java \
  src/main/resources/application.yml \
  src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java
do
  if [ -f "$f" ]; then
    echo "-- $f"
    cat -n "$f"
  fi
done

printf '%s\n' '== Parent-relative diff =='
git diff --find-renames --find-copies --unified=50 "$base" "$head" -- \
  CHANGELOG.md \
  docs/security/2026-08-06-durable-artifact-deletion-receipts.md \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java \
  src/main/resources/application.yml \
  src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java

printf '%s\n' '== Lifecycle usage and missing-tenant hash lookup contexts =='
rg -n -C 4 --glob '*.java' \
  '(ArtifactDeletionReceiptStore|ArtifactDeletionLedger|ArtifactDeletionReceipt|ArtifactDeletionState|contentHash|content_hash|tenantId|tenant_id)' \
  src/main/java || true

Length of output: 194841


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='2f6bc8ddada68d73c7b35641d005a7fe02ee9c0a'
base='3e98e6fbf8af006f08e50463974468e89daf168d'
log='/tmp/coderabbit-shell-logs/shell-output-nyZMKE'

printf '%s\n' '== Exact revision and changed paths =='
git rev-parse HEAD
printf 'merge-base: '; git merge-base "$head" "$base"
printf 'parent-relative commits: '; git rev-list --count "$base..$head"
git diff --name-status "$base" "$head"

printf '%s\n' '== Ledger and receipt source with line numbers =='
cat -n src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java
cat -n src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java

printf '%s\n' '== Store, state, ADR, and changelog =='
cat -n src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java
cat -n src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java
cat -n docs/security/2026-08-06-durable-artifact-deletion-receipts.md
git diff --unified=20 "$base" "$head" -- CHANGELOG.md src/main/resources/application.yml

printf '%s\n' '== Receipt ledger references outside the new package =='
rg -n -C 3 --glob '*.java' \
  '(new\s+ArtifactDeletionLedger\s*\(|ArtifactDeletionReceiptStore|ArtifactDeletionLedger)' \
  src/main/java src/test/java || true

printf '%s\n' '== Parent hardening preservation =='
git diff --unified=30 "$base" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

printf '%s\n' '== Changed production path categories =='
git diff --name-only "$base" "$head" | \
  awk '
    /\/controller\// { print "controller: " $0 }
    /\/repository\// { print "repository: " $0 }
    /\/artifact\// { print "artifact: " $0 }
    /\/service\// { print "service: " $0 }
    /scheduler|workflow|pom\.xml|build\.gradle|package\.json|Dockerfile|\.github\// { print "excluded-category: " $0 }
  ' || true

printf '%s\n' '== Prior output revision/document excerpts, if available =='
if [ -f "$log" ]; then
  rg -n -C 3 'Revision verification|Parent-relative changed paths|CHANGELOG|durable-artifact-deletion-receipts|^[0-9a-f]{40}$|^[AM]\s' "$log" || true
fi

Length of output: 76230


@seonghobae I found one actionable replay-validation defect in exact head 2f6bc8ddada68d73c7b35641d005a7fe02ee9c0a.

  • [P1] Reject impossible cleanup-attempt fields during receipt replay. ArtifactDeletionReceipt.validateStateFields at Line 198 only requires lastAttemptAt not to precede requestedAt. It therefore accepts a DELETION_REQUESTED snapshot with attemptCount == 0 and a non-null lastAttemptAt. ArtifactDeletionLedger.replayLine at Line 260 accepts this as the first snapshot because it checks only the state and attempt count. The malformed attempt timestamp then remains in the replayed receipt and can flow through later snapshots. This contradicts the documented fail-closed state-shape validation and permits evidence of cleanup activity before cleanup is pending. Require no attempt timestamp when attemptCount is zero, require an attempt timestamp when the count is positive, reject attempt evidence for DELETION_REQUESTED and METADATA_TOMBSTONED, and require lastAttemptAt not to be after the current state time. Add malformed-ledger replay tests for these cases.

I also verified the following:

  • The exact merge base is 3e98e6fbf8af006f08e50463974468e89daf168d.
  • The parent-relative range contains one commit and the declared eight files only.
  • The change adds no controller, existing repository mutation, artifact-store deletion, worker, scheduler, workflow, dependency, or release change.
  • The ADR and changelog correctly retain the incomplete-cleanup finding. They do not claim that HTTP deletion, atomic tombstoning, signed-link revocation, or cleanup work is complete.
  • The parent’s missing-tenant content-hash hardening is unchanged.
  • The receipt contract otherwise implements the requested identity binding, same-job conflict rejection, transition ordering, terminal completion, bounded UTF-8 serialization, forced append, and completed-receipt exclusion from pending work.

This remains a Draft. I did not infer CI success or counted independent approval.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java (2)

285-299: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

채널 재사용을 검토하십시오(선택).

append는 전이마다 createDirectories를 호출하고 채널을 새로 열고 닫습니다. 현재 슬라이스의 쓰기 빈도는 낮으므로 동작상 문제는 없습니다.

cleanup worker가 통합되어 재시도 쓰기가 늘어나면, 채널을 한 번 열어 보관하고 force(true)만 반복하는 방식이 syscall을 줄입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java`
around lines 285 - 299, Optionally optimize append in ArtifactDeletionLedger by
reusing a single open FileChannel across repeated writes instead of creating
directories and opening/closing the channel for every transition. Preserve the
existing ByteBuffer write loop and invoke force(true) after each append; retain
safe channel lifecycle management for cleanup.

281-283: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

append의 도달 불가 oversized 검사를 제거하거나 테스트 가능하게 변경하십시오.

유효한 ArtifactDeletionReceipt의 직렬화 결과는 MAX_LEDGER_LINE_BYTES보다 작습니다. 현재 테스트는 load 경로만 검증하므로 append의 true 분기는 실행할 수 없습니다. 상한 검사는 replay 경로에 유지하고, append 검사에는 테스트 가능한 경계를 제공하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java`
around lines 281 - 283, ArtifactDeletionLedger의 append 경로에서 유효한
ArtifactDeletionReceipt로는 도달할 수 없는 MAX_LEDGER_LINE_BYTES 초과 검사를 제거하십시오.
oversizedLine() 검사는 replay를 수행하는 load 경로에만 유지하고, append는 정상적인 직렬화 결과를 그대로 기록하도록
하십시오.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/security/2026-08-06-durable-artifact-deletion-receipts.md`:
- Around line 19-21: 문서의 해당 문단을 다시 줄바꿈하여 `#263`이 줄의 첫 토큰이 되지 않도록 수정하십시오. `MD018`
위반만 해결하고, 기존 문구와 의미는 유지하십시오.

In `@src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java`:
- Line 284: Update the serialization write in ArtifactDeletionLedger to append a
fixed "\n" record terminator instead of System.lineSeparator(). Keep UTF-8
encoding unchanged so durable ledger files are host-independent and remain
compatible with replayBytes/readBoundedLines.
- Around line 241-243: load()의 파일 재생 로직을 수정하여 마지막 레코드가 개행으로 종결된 경우에만
replayBytes로 재생하십시오. 개행 없는 마지막 바이트열은 discardTornRecord로 마지막 \n 위치까지 truncate하고
Log4j2 warning을 기록한 뒤 무시하며, 완전한 레코드의 기존 재생 동작은 유지하십시오.

In
`@src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java`:
- Around line 222-246: Expand ArtifactDeletionLedgerTest with
direct-ledger-input failure tests covering strictUtf8 invalid-byte rejection,
replayLine immutable-identity conflicts, validateReplayTransition non-monotonic
timestamps and illegal subsequent states, and decodeRequired whitespace fields.
Assert each case throws IllegalStateException with the corresponding validation
message, while preserving the existing malformed-line and oversized-line tests.

---

Nitpick comments:
In `@src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java`:
- Around line 285-299: Optionally optimize append in ArtifactDeletionLedger by
reusing a single open FileChannel across repeated writes instead of creating
directories and opening/closing the channel for every transition. Preserve the
existing ByteBuffer write loop and invoke force(true) after each append; retain
safe channel lifecycle management for cleanup.
- Around line 281-283: ArtifactDeletionLedger의 append 경로에서 유효한
ArtifactDeletionReceipt로는 도달할 수 없는 MAX_LEDGER_LINE_BYTES 초과 검사를 제거하십시오.
oversizedLine() 검사는 replay를 수행하는 load 경로에만 유지하고, append는 정상적인 직렬화 결과를 그대로 기록하도록
하십시오.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b6a3a0f-e979-4ee2-ae3e-0707f6820a3f

📥 Commits

Reviewing files that changed from the base of the PR and between 3e98e6f and 2f6bc8d.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/security/2026-08-06-durable-artifact-deletion-receipts.md
  • src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java
  • src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java
  • src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java
  • src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java
  • src/main/resources/application.yml
  • src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java

Comment thread docs/security/2026-08-06-durable-artifact-deletion-receipts.md Outdated
Comment thread src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java Outdated

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Please independently review exact current head 0a447f1dc9cc0afaeb846ddd42999e9319e18d4e only. This Draft is exactly 5 commits ahead and 0 behind parent #268 head 3e98e6fbf8af006f08e50463974468e89daf168d, with the parent as merge base and nine changed files.

Verify the complete RECEIPT_V1 tenant/artifact identity contract, monotonic lifecycle validation, strict UTF-8 and bounded replay, privacy-safe controlled failure evidence, exact duplicate idempotence, permanent same-job conflict rejection, and standalone/MSA boundary. Pay particular attention to the test-first crash-tail correction: RED e10ceee4ab6fed8cc35fa8732fb25caadae91a4d removes the final record delimiter and requires restart failure; GREEN 44dd0b40ddb6ea34c0c773ab317fe3622fe899fc rejects every non-empty unterminated EOF tail instead of promoting potentially torn or pre-force evidence. Confirm that line termination is a valid commit delimiter for the implemented append protocol and that committed records still replay correctly.

Exact-head fuzz run 31091949750 is currently in progress. CI, Security Scan, SAST, Strix, full Maven/JaCoCo/Javadoc evidence, formal exact-head reviews, and counted independent write-authorized approval are absent or pending and are not passing. Treat every predecessor-head result as stale. This foundation still does not implement atomic tombstoning, outbox publication, cleanup execution, signed-link revocation, or truthful completed-deletion semantics; do not resolve #268's incomplete-cleanup thread based on this PR.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='0a447f1dc9cc0afaeb846ddd42999e9319e18d4e'
base='3e98e6fbf8af006f08e50463974468e89daf168d'

echo '== Exact revision topology =='
printf 'HEAD: '; git rev-parse HEAD
printf 'head type: '; git cat-file -t "$head"
printf 'base type: '; git cat-file -t "$base"
printf 'merge-base: '; git merge-base "$head" "$base"
printf 'ahead/behind: '; git rev-list --left-right --count "$base...$head"
printf 'commits in range:\n'; git rev-list --reverse --oneline "$base..$head"

echo '== Parent-relative changed files =='
git diff --name-status "$base" "$head"
git diff --stat "$base" "$head"

echo '== Candidate file map =='
fd -t f . src/main/java/com/clearfolio/viewer/lifecycle src/test/java/com/clearfolio/viewer/lifecycle docs/security 2>/dev/null || true
for f in \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java \
  src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java
do
  if [ -f "$f" ]; then
    echo "-- outline: $f"
    ast-grep outline "$f" --items all || true
  fi
done

echo '== Parent-relative diff: receipt implementation and tests =='
git diff --unified=35 "$base" "$head" -- \
  src/main/java/com/clearfolio/viewer/lifecycle \
  src/test/java/com/clearfolio/viewer/lifecycle \
  docs/security \
  CHANGELOG.md \
  src/main/resources/application.yml

echo '== Focused replay, append, validation, and test contexts =='
rg -n -C 12 --glob '*.java' \
  '(replay|readLine|EOF|unterminated|newline|\\n|force\s*\(|FileChannel|RECEIPT_V1|validateStateFields|attemptCount|lastAttemptAt|failureCode|hasSameIdentity|request\s*\()' \
  src/main/java/com/clearfolio/viewer/lifecycle \
  src/test/java/com/clearfolio/viewer/lifecycle || true

echo '== Receipt references outside lifecycle package =='
rg -n -C 4 --glob '*.java' \
  '(ArtifactDeletionReceiptStore|ArtifactDeletionLedger|ArtifactDeletionReceipt|ArtifactDeletionState)' \
  src/main/java src/test/java || true

echo '== Changed prohibited-category paths and parent hardening diff =='
git diff --name-only "$base" "$head" | rg -n \
  '(^|/)(controller|repository|worker|scheduler|workflow|artifact)(/|$)|pom\.xml$|build\.gradle|package\.json$|(^|/)Dockerfile$|^\.github/|release' || true
git diff --unified=20 "$base" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java || true

Length of output: 203309


🏁 Script executed:

#!/bin/bash
set -euo pipefail

log='/tmp/coderabbit-shell-logs/shell-output-F74lpv'
head='0a447f1dc9cc0afaeb846ddd42999e9319e18d4e'
base='3e98e6fbf8af006f08e50463974468e89daf168d'

echo '== Revision and changed-file evidence from full log =='
sed -n '1,180p' "$log"

echo '== Exact changed paths =='
git diff --name-status "$base" "$head"

echo '== Full receipt state validation =='
sed -n '35,225p' src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java

echo '== Full crash-tail test =='
sed -n '1,130p' src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerCrashTailTest.java

echo '== Attempt-evidence test cases and replay mutations =='
rg -n -C 8 --glob '*.java' \
  '(lastAttemptAt|attemptCount|replayRejects|invalidFirst|unterminated|truncate|setLength|deleteCharAt)' \
  src/test/java/com/clearfolio/viewer/lifecycle

echo '== Append/replay boundary implementation =='
sed -n '214,410p' src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java

echo '== Documentation scope claims and references =='
rg -n -C 3 \
  '(atomic|outbox|worker|signed-link|incomplete|complete cleanup|RECEIPT_V1|References|APA|tombstone)' \
  CHANGELOG.md docs/security/2026-08-06-durable-artifact-deletion-receipts.md || true

echo '== Exact parent-hardening source status =='
git diff --name-status "$base" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

Length of output: 66190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='0a447f1dc9cc0afaeb846ddd42999e9319e18d4e'
checkout='50d7b25058fe0419e0a3a886c6238ec124332695'

echo '== Relationship between sandbox checkout and requested head =='
printf 'requested head: '; git rev-parse "$head"
printf 'checkout head: '; git rev-parse "$checkout"
printf 'merge-base: '; git merge-base "$head" "$checkout"
printf 'requested-head..checkout commits:\n'
git log --oneline "$head..$checkout" || true
printf 'checkout..requested-head commits:\n'
git log --oneline "$checkout..$head" || true

echo '== Exact requested-head receipt validation =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java" | sed -n '35,225p'

echo '== Exact requested-head replay and append implementation =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java" | sed -n '214,410p'

echo '== Exact requested-head crash-tail test =='
git show "$head:src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerCrashTailTest.java" | sed -n '1,130p'

echo '== Exact requested-head attempt-evidence test coverage =='
git grep -n -E \
  '(lastAttemptAt|attemptCount|unterminatedFinalRecord|replayRejects)' \
  "$head" -- src/test/java/com/clearfolio/viewer/lifecycle || true

echo '== Exact requested-head implementation references outside lifecycle =='
git grep -n -E \
  '(ArtifactDeletionReceiptStore|ArtifactDeletionLedger|ArtifactDeletionReceipt|ArtifactDeletionState)' \
  "$head" -- ':!src/main/java/com/clearfolio/viewer/lifecycle' ':!src/test/java/com/clearfolio/viewer/lifecycle' || true

Length of output: 22516


@seonghobae I reviewed exact head 0a447f1dc9cc0afaeb846ddd42999e9319e18d4e against parent 3e98e6fbf8af006f08e50463974468e89daf168d.

I found one actionable finding.

  • [P1] Reject inconsistent cleanup-attempt evidence during replay. ArtifactDeletionReceipt.validateStateFields at Line 198 validates lastAttemptAt only against requestedAt. It accepts a DELETION_REQUESTED record with attemptCount == 0 and a non-null lastAttemptAt. It also accepts a METADATA_TOMBSTONED record with cleanup-attempt evidence, and states with a positive attemptCount but no attempt timestamp. ArtifactDeletionLedger.replayLine at Line 260 accepts the malformed first record because it checks only DELETION_REQUESTED and attemptCount == 0. This permits impossible cleanup evidence before cleanup becomes pending. Require:
    • lastAttemptAt == null when attemptCount == 0;
    • lastAttemptAt != null when attemptCount > 0;
    • no attempt evidence in DELETION_REQUESTED or METADATA_TOMBSTONED;
    • lastAttemptAt <= stateChangedAt;
    • malformed-ledger replay tests for each invalid combination.

I also verified the following by reading the requested commit object directly. The sandbox checkout had one later commit, so I did not use it as exact-head evidence.

  • The requested head is five commits ahead and zero behind the parent. The merge base is the declared parent.
  • The parent-relative diff changes nine files.
  • The line terminator is a valid commit delimiter for this append protocol. append writes the complete record and delimiter, then calls FileChannel.force(true).
  • readBoundedLines rejects every non-empty EOF buffer. Therefore, it does not replay an unterminated final record.
  • ArtifactDeletionLedgerCrashTailTest removes the final delimiter and requires restart failure.
  • The existing restart replay test confirms that delimiter-terminated committed records replay.
  • The contract includes tenant, request, job, SHA-256 artifact checksum, audit-correlation identity, exact duplicate idempotence, same-job conflict rejection, controlled failure-code format, bounded strict UTF-8 parsing, and terminal completion.
  • The diff adds no controller, repository deletion, artifact deletion, worker, scheduler, workflow, dependency, or release change.
  • The parent missing-tenant content-hash hardening is unchanged.
  • The ADR and changelog retain the incomplete-cleanup limitation. They do not claim atomic tombstoning, outbox publication, cleanup execution, signed-link revocation, or completed-deletion semantics.

This PR remains Draft. I did not infer success from the pending fuzz run or from any absent CI, security, review, coverage, or approval evidence. I did not resolve the incomplete-cleanup finding.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Fix only the deterministic setup bug in ArtifactDeletionLedgerCoverageTest.loadAndWriteIoFailuresAreReportedWithoutPartialSuccess on exact branch head 50d7b25058fe0419e0a3a886c6238ec124332695.

The current write-failure setup constructs ArtifactDeletionLedger with a path whose parent is already a regular file, so the constructor can fail during load before the intended append path is exercised. Change the setup to:

  1. create an empty real parent directory;
  2. construct the ledger for a missing child path inside it, proving startup sees an empty store;
  3. delete that empty parent directory and replace it with a regular file;
  4. call request(...) and require artifact deletion ledger cannot be written;
  5. retain the assertion that no receipt became visible.

Do not alter production code, other tests, workflow files, dependencies, PR metadata, or the Draft state. Run the focused test if the environment permits and report the exact resulting head.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Re-review exact current head 8c8cd53a102ac923ea862f6e7ea33e6f9da7b3c8 relative to parent #268 exact 3e98e6fbf8af006f08e50463974468e89daf168d. Treat every predecessor-head conclusion as stale.

The previously reported replay-validation defect is addressed in ArtifactDeletionReceipt.validateStateFields: zero attempts require no lastAttemptAt; positive attempts require one; DELETION_REQUESTED and METADATA_TOMBSTONED reject cleanup-attempt evidence; and lastAttemptAt cannot follow stateChangedAt. Dedicated malformed-ledger replay tests exercise these combinations. The crash-tail contract also rejects non-empty unterminated EOF evidence because the line delimiter is the append commit boundary.

Independently verify the current 11-file parent-relative slice, strict bounded RECEIPT_V1 replay, immutable identity, monotonic state/timestamps, controlled privacy-safe failure evidence, append + force(true), restart reconstruction, exact duplicate idempotence, invalid-attempt rejection, crash-tail rejection, complete public Javadocs, and the deliberate absence of endpoint/controller/delete-worker/outbox integration. Keep the incomplete-cleanup finding open under #263.

Exact-head fuzz run 31092336203 was cancelled and is not passing; do not infer acceptance from predecessor runs. Report exact-head actionable findings or a no-blocker advisory conclusion. Keep Draft and do not infer counted independent approval.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='8c8cd53a102ac923ea862f6e7ea33e6f9da7b3c8'
base='3e98e6fbf8af006f08e50463974468e89daf168d'

echo '== Exact revision topology =='
printf 'requested head: '; git rev-parse "$head"
printf 'parent: '; git rev-parse "$base"
printf 'merge-base: '; git merge-base "$head" "$base"
printf 'ahead/behind: '; git rev-list --left-right --count "$base...$head"
echo '== Changed paths =='
git diff --name-status "$base" "$head"

echo '== Changed Java file map =='
git diff --name-only "$base" "$head" -- '*.java' |
while IFS= read -r file; do
  echo "-- $file"
  git show "$head:$file" > "/tmp/$(basename "$file")"
  ast-grep outline "/tmp/$(basename "$file")" --items all || true
done

echo '== Receipt model validation and transitions =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java" |
  nl -ba | sed -n '1,330p'

echo '== Ledger append, encoding, and replay =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java" |
  nl -ba | sed -n '1,520p'

echo '== Public contract and state =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java" |
  nl -ba | sed -n '1,260p'
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java" |
  nl -ba | sed -n '1,160p'

echo '== Exact-head lifecycle tests and crash-tail test =='
git ls-tree -r --name-only "$head" src/test/java/com/clearfolio/viewer/lifecycle |
  rg 'ArtifactDeletion(Ledger|Receipt).*Test\.java$' |
while IFS= read -r file; do
  echo "-- $file"
  git show "$head:$file" | nl -ba
done

echo '== Exact-head diff for docs/config/changelog =='
git diff --unified=30 "$base" "$head" -- \
  CHANGELOG.md \
  docs/security/2026-08-06-durable-artifact-deletion-receipts.md \
  src/main/resources/application.yml

echo '== Scope and parent-hardening preservation =='
git diff --name-only "$base" "$head" |
  rg '(^|/)(controller|repository|worker|scheduler|workflow|artifact)(/|$)|(^|/)(pom\.xml|build\.gradle|package\.json|Dockerfile)$|^\.github/|release' || true
git diff --name-status "$base" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

echo '== Receipt integration references outside lifecycle =='
git grep -n -E '(ArtifactDeletionReceiptStore|ArtifactDeletionLedger|ArtifactDeletionReceipt|ArtifactDeletionState)' \
  "$head" -- ':!src/main/java/com/clearfolio/viewer/lifecycle' ':!src/test/java/com/clearfolio/viewer/lifecycle' || true

Length of output: 11193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='8c8cd53a102ac923ea862f6e7ea33e6f9da7b3c8'
base='3e98e6fbf8af006f08e50463974468e89daf168d'
numbered() { awk '{ printf "%6d\t%s\n", NR, $0 }'; }

echo '== Receipt model validation and transitions =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java" | numbered

echo '== Ledger implementation =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java" | numbered

echo '== Contract and state =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java" | numbered
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java" | numbered

echo '== Replay validation tests =='
git show "$head:src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerReplayValidationTest.java" | numbered

echo '== Crash-tail test =='
git show "$head:src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerCrashTailTest.java" | numbered

echo '== Main ledger tests =='
git show "$head:src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java" | numbered

echo '== Coverage and I/O tests =='
git show "$head:src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerCoverageTest.java" | numbered

echo '== Documentation, changelog, and configuration diff =='
git diff --unified=20 "$base" "$head" -- \
  CHANGELOG.md \
  docs/security/2026-08-06-durable-artifact-deletion-receipts.md \
  src/main/resources/application.yml

echo '== Scope exclusions and parent hardening =='
printf 'prohibited changed paths:\n'
git diff --name-only "$base" "$head" |
  grep -E '(^|/)(controller|repository|worker|scheduler|workflow|artifact)(/|$)|(^|/)(pom\.xml|build\.gradle|package\.json|Dockerfile)$|^\.github/|release' || true
printf 'parent hardening paths changed:\n'
git diff --name-status "$base" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java
printf 'receipt references outside lifecycle package:\n'
git grep -n -E '(ArtifactDeletionReceiptStore|ArtifactDeletionLedger|ArtifactDeletionReceipt|ArtifactDeletionState)' \
  "$head" -- ':!src/main/java/com/clearfolio/viewer/lifecycle' ':!src/test/java/com/clearfolio/viewer/lifecycle' || true

Length of output: 107007


@seonghobae I reviewed exact head 8c8cd53a102ac923ea862f6e7ea33e6f9da7b3c8 against parent 3e98e6fbf8af006f08e50463974468e89daf168d.

I found one actionable finding.

  • [P1] Set up the append failure after successful ledger construction. In ArtifactDeletionLedgerCoverageTest.loadAndWriteIoFailuresAreReportedWithoutPartialSuccess, Line 238 creates parent_file as a regular file. Line 240 then constructs ArtifactDeletionLedger for parent_file/ledger.log. The constructor calls load(), and Files.newInputStream(...) fails before request(...) runs. ArtifactDeletionLedger.load() converts that failure to "artifact deletion ledger cannot be loaded" at Line 223. Therefore, the test cannot assert the intended append failure "artifact deletion ledger cannot be written" or prove that no receipt became visible. Create an empty parent directory, construct the ledger for a missing child, replace the empty directory with a regular file, then call request(...).

The previously reported replay-validation defect is fixed. ArtifactDeletionReceipt.validateStateFields now rejects inconsistent attempt evidence and attempt timestamps after stateChangedAt. The dedicated replay tests cover invalid requested-state evidence and erased retry evidence.

I also verified these items:

  • The exact head is 11 commits ahead and 0 behind the declared parent.
  • The parent-relative slice changes 11 files.
  • The ledger writes a complete LF-terminated record and calls FileChannel.force(true) before it exposes a transition.
  • Replay rejects every non-empty unterminated EOF tail.
  • The crash-tail test removes the LF delimiter and requires restart failure.
  • The receipt identity, same-job conflict rejection, state ordering, timestamp monotonicity, controlled failure-code format, strict UTF-8 decoding, bounded records, and restart reconstruction are present.
  • Completed receipts remain retained and are excluded from pending work.
  • The diff does not change controllers, repositories, artifact deletion, workers, schedulers, workflows, dependencies, or releases.
  • The parent missing-tenant content-hash hardening paths are unchanged.
  • The documentation keeps the incomplete-cleanup limitation open under #263.

This PR remains Draft. I did not infer success from the cancelled fuzz run or from absent CI, security, SAST, coverage, Javadoc, review, or approval evidence.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Please independently review exact current head 4e67e2aff953a9547fb284eb5e15aa3b3c7d334a only. It is 14 commits ahead and 0 behind parent #268 exact head 3e98e6fbf8af006f08e50463974468e89daf168d, with that parent as merge base and exactly 11 changed files.

Re-evaluate all predecessor feedback against the current source. In particular, verify:

  • fixed LF is the host-independent RECEIPT_V1 commit delimiter;
  • non-empty unterminated EOF tails are rejected and preserved for controlled forensic recovery rather than replayed or silently truncated;
  • attempt_count and last_attempt_at have consistent presence and temporal order;
  • requested/tombstoned states cannot claim cleanup attempts;
  • retry-pending and completion preserve prior attempt evidence exactly;
  • only pending-to-failed increments attempt count and advances attempt time;
  • malformed UTF-8, required fields, identity conflicts, illegal/non-monotonic transitions, I/O failures, and crash tails are covered deterministically;
  • public lifecycle APIs remain beginner-readable and the package remains standalone/MSA-replaceable.

Exact-head fuzz run 31092645274 succeeded. CI, Security Scan, SAST, Strix, merge-compatible full Maven/JaCoCo/Javadoc evidence, final exact-head reviews, and counted independent write-authorized approval remain absent or pending and are not passing. Do not infer complete artifact deletion: atomic tombstone/outbox, cleanup execution, signed-link revocation, recovery operations, truthful status API, accessible UI, and real PDF/storage restart evidence remain under #263. Keep #268's incomplete-cleanup thread open.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Re-evaluate exact ledger head 4e67e2aff953a9547fb284eb5e15aa3b3c7d334a against the current parent #268 head d57f1aa8153aa092607a147c9587c683f009ea06, not predecessor parent 3e98e6fbf8af006f08e50463974468e89daf168d.

Fresh comparison is 14 commits ahead, 1 behind, with merge base 3e98e6fbf8af006f08e50463974468e89daf168d. The effective ledger slice remains the same 11 files. The one parent-only commit corrects tests to require null and blank scoped tenant content lookup to fail closed while preserving the explicit legacy demo lookup. Verify that reconciling this stack will retain that parent contract and will not duplicate or overwrite parent security evidence.

Review the receipt ledger’s immutable identity binding, monotonic/non-reversible lifecycle, attempt-count/time invariants, exact duplicate idempotency, same-job identity-conflict rejection, strict UTF-8 and bounded replay, fixed LF commit delimiter, fail-closed preservation of unterminated/corrupt evidence, controlled failure codes, standalone/MSA adapter boundary, complete public Javadocs, and 100% production line/branch evidence. All current inline CodeRabbit threads are resolved; do not reopen the rejected automatic-truncation proposal unless new evidence shows that preserving corrupt audit bytes is unsafe.

Exact-head fuzz run 31092645274 succeeded for the ledger source tree, but it is not current-parent compatibility evidence. CI, Security Scan, SAST, Strix, current-parent CodeRabbit, OpenCode/Noema, full Maven/JaCoCo/Javadoc, and independent approval remain absent or stale. Keep Draft. This receipt-only foundation does not resolve #268’s incomplete artifact cleanup until transactional tombstone/outbox integration, revocation, durable worker retry/recovery, truthful API/UI state, and real storage/restart evidence exist.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='4e67e2aff953a9547fb284eb5e15aa3b3c7d334a'
parent='d57f1aa8153aa092607a147c9587c683f009ea06'
merge_base='3e98e6fbf8af006f08e50463974468e89daf168d'

echo '== Exact topology =='
printf 'checkout: '; git rev-parse HEAD
printf 'ledger head: '; git rev-parse "$head"
printf 'current parent: '; git rev-parse "$parent"
printf 'merge base: '; git merge-base "$head" "$parent"
printf 'ahead/behind: '; git rev-list --left-right --count "$parent...$head"
printf 'ledger range commits: '; git rev-list --count "$merge_base..$head"
printf 'parent-only commits: '; git rev-list --count "$merge_base..$parent"

echo '== Three-way path comparison =='
echo '-- ledger slice versus merge base'
git diff --name-status "$merge_base" "$head"
echo '-- parent-only changes versus merge base'
git diff --name-status "$merge_base" "$parent"
echo '-- current parent versus ledger head'
git diff --name-status "$parent" "$head"

echo '== Receipt source outlines at exact ledger head =='
for f in \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java
do
  echo "-- $f"
  git show "$head:$f" > "/tmp/$(basename "$f")"
  ast-grep outline "/tmp/$(basename "$f")" --items all || true
done

echo '== Receipt and ledger implementation =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java" | nl -ba
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java" | nl -ba
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java" | nl -ba
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java" | nl -ba

echo '== Exact ledger-head lifecycle tests =='
git ls-tree -r --name-only "$head" src/test/java/com/clearfolio/viewer/lifecycle |
  rg 'ArtifactDeletion.*Test\.java$' |
  while IFS= read -r f; do
    echo "-- $f"
    git show "$head:$f" | nl -ba
  done

echo '== Parent-only tenant hardening diff =='
git diff --unified=40 "$merge_base" "$parent" -- \
  src/test/java \
  src/main/java/com/clearfolio/viewer/repository \
  src/main/java/com/clearfolio/viewer/service || true

echo '== Ledger changes relative to merge base and external references =='
git diff --unified=25 "$merge_base" "$head" -- \
  src/main/java/com/clearfolio/viewer/lifecycle \
  src/test/java/com/clearfolio/viewer/lifecycle \
  docs/security \
  CHANGELOG.md \
  src/main/resources/application.yml
git grep -n -E '(ArtifactDeletionReceiptStore|ArtifactDeletionLedger|ArtifactDeletionReceipt|ArtifactDeletionState)' \
  "$head" -- ':!src/main/java/com/clearfolio/viewer/lifecycle' ':!src/test/java/com/clearfolio/viewer/lifecycle' || true

echo '== Coverage and build configuration references at exact ledger head =='
git ls-tree -r --name-only "$head" | rg '(^|/)(pom\.xml|build\.gradle|build\.gradle\.kts)$' |
  while IFS= read -r f; do
    echo "-- $f"
    git show "$head:$f" | rg -n -C 5 '(jacoco|coverage|javadoc|maven-surefire|maven-failsafe)' || true
  done

Length of output: 8432


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='4e67e2aff953a9547fb284eb5e15aa3b3c7d334a'
parent='d57f1aa8153aa092607a147c9587c683f009ea06'
base='3e98e6fbf8af006f08e50463974468e89daf168d'
numbered() { awk '{ printf "%6d\t%s\n", NR, $0 }'; }

echo '== Exact receipt model =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java" | numbered

echo '== Exact ledger implementation =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java" | numbered

echo '== Exact contract and state enum =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java" | numbered
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java" | numbered

echo '== Exact ledger-head tests =='
for f in \
  src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerTest.java \
  src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerReplayValidationTest.java \
  src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerCrashTailTest.java \
  src/test/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedgerCoverageTest.java
do
  echo "-- $f"
  git show "$head:$f" | numbered
done

echo '== Parent-only tenant lookup hardening =='
git diff --unified=35 "$base" "$parent" -- \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTest.java

echo '== Ledger source has no integration outside lifecycle package =='
git grep -n -E '(ArtifactDeletionReceiptStore|ArtifactDeletionLedger|ArtifactDeletionReceipt|ArtifactDeletionState)' \
  "$head" -- ':!src/main/java/com/clearfolio/viewer/lifecycle' ':!src/test/java/com/clearfolio/viewer/lifecycle' || true

echo '== Build coverage configuration at ledger head =='
git ls-tree -r --name-only "$head" |
  grep -E '(^|/)(pom\.xml|build\.gradle|build\.gradle\.kts)$' |
  while IFS= read -r f; do
    echo "-- $f"
    git show "$head:$f" |
      grep -n -E -C 5 '(jacoco|coverage|javadoc|maven-surefire|maven-failsafe)' || true
  done

echo '== Documentation scope and replay claims =='
git show "$head:docs/security/2026-08-06-durable-artifact-deletion-receipts.md" |
  grep -n -E -C 3 '(LF|UTF-8|unterminated|corrupt|force|atomic|outbox|worker|incomplete|References)' || true
git show "$head:CHANGELOG.md" | grep -n -E -C 2 '(receipt|cleanup|tombstone|outbox)' || true

Length of output: 109836


@seonghobae I reviewed exact ledger head 4e67e2aff953a9547fb284eb5e15aa3b3c7d334a against current parent d57f1aa8153aa092607a147c9587c683f009ea06.

I found one actionable finding.

  • [P1] Construct the ledger before replacing its parent directory with a file. In ArtifactDeletionLedgerCoverageTest.loadAndWriteIoFailuresAreReportedWithoutPartialSuccess, Line 238 creates parent_file as a regular file. Line 240 then constructs ArtifactDeletionLedger for parent_file/ledger.log. The constructor calls load(). Files.newInputStream(...) fails before request(...) executes. The observable error is therefore "artifact deletion ledger cannot be loaded", not the asserted "artifact deletion ledger cannot be written". The test does not exercise append failure or prove no partial write. Create an empty parent directory and construct the ledger for a missing child path. Then replace the empty directory with a regular file. Finally, call request(...) and assert the write error and absent receipt.

Static inspection confirmed the following:

  • The merge base is 3e98e6fbf8af006f08e50463974468e89daf168d.
  • The ledger head is 14 commits ahead and 1 commit behind the current parent.
  • The ledger slice changes the declared 11 files.
  • The parent-only change is src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTest.java.
  • That parent test requires findByTenantAndContentHash to reject null and blank tenants. It preserves explicit legacy findByContentHash lookup. The ledger slice does not modify this file. Reconciliation must retain this parent-only change.
  • ArtifactDeletionReceipt validates immutable tenant, request, job, SHA-256 digest, audit correlation, and request-time identity.
  • Duplicate requests return the existing receipt only when all immutable identity fields match. Conflicting reuse of a job ID fails closed.
  • The lifecycle rejects reverse and terminal transitions. The replay validator preserves attempt evidence across failed-to-pending and pending-to-completed transitions. Only pending-to-failed increments the attempt count.
  • lastAttemptAt must match attempt-count presence, cannot precede requestedAt, and cannot follow stateChangedAt. Requested and tombstoned receipts cannot carry cleanup-attempt evidence.
  • The ledger writes UTF-8 RECEIPT_V1 records with a fixed "\n" delimiter and calls FileChannel.force(true) before updating visible state.
  • Replay rejects malformed UTF-8, blank records, oversized records, identity conflicts, illegal transitions, non-monotonic timestamps, and non-empty unterminated tails. It preserves corrupt bytes because it does not truncate or rewrite the ledger.
  • The crash-tail test verifies the LF delimiter and restart failure after delimiter removal.
  • The store interface remains a standalone and MSA-replaceable boundary. Public lifecycle APIs have Javadocs.
  • No production integration exists outside the lifecycle package. The receipt ledger does not implement tombstone/outbox atomicity, cleanup execution, link revocation, recovery operations, status API/UI state, or storage restart evidence.

The pom.xml configures JaCoCo checks and Javadoc validation. I could not verify the claimed 100% production line and branch result because full Maven and JaCoCo execution evidence is unavailable in this inspection.

This PR remains Draft. The successful fuzz run does not provide current-parent compatibility evidence. I did not infer CI, security, SAST, Strix, full Maven, coverage, Javadoc, review, or independent approval success. The incomplete artifact-cleanup finding remains open.


✏️ Learnings added
Learnt from: seonghobae
URL: https://github.com/ContextualWisdomLab/clearfolio/pull/277

Timestamp: 2026-08-06T10:32:16.536Z
Learning: In Clearfolio, `InMemoryConversionJobRepository.findByTenantAndContentHash` must fail closed for null or blank tenant identifiers. Only the explicit legacy `findByContentHash` lookup may use the demo-tenant scope. This contract is covered by `src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTest.java`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@seonghobae
seonghobae force-pushed the feat/durable-deletion-receipt-ledger branch from 1861049 to 8448fdd Compare August 6, 2026 11:07

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Please independently review exact current head 8448fdd98d4a6c58f4e5407fcfcce903785abaa8 only. It is a clean two-commit test-first stack on parent #268 exact head d57f1aa8153aa092607a147c9587c683f009ea06, 2 commits ahead and 0 behind, with exactly 11 ledger-scoped files. Exact-head fuzz run 31096007229 succeeded across all three targets.

Verify immutable receipt identity, tenant and permanently reserved job binding, idempotency, monotonic and non-reversible lifecycle transitions, strict UTF-8 bounded replay, fixed-LF commit delimiters, FileChannel.force(true) durability, fail-closed preservation of unterminated forensic tails, attempt-count/time invariants, privacy-safe controlled failure evidence, beginner-readable public documentation, standalone/MSA adapter boundaries, and preservation of the parent’s explicit-tenant lookup contract. Confirm this foundation does not claim to resolve incomplete artifact cleanup and does not alter endpoints, existing repository mutation, artifact-store mutation, workflows, dependencies, versions, or releases. Treat every predecessor review and check as stale. Submit formal exact-head findings or approval only after applicable gates are complete; counted independent write-authorized approval and all branch protections remain separate mandatory requirements.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='8448fdd98d4a6c58f4e5407fcfcce903785abaa8'
parent='d57f1aa8153aa092607a147c9587c683f009ea06'

numbered() { awk '{ printf "%6d\t%s\n", NR, $0 }'; }

echo '== Exact revision topology =='
printf 'sandbox checkout: '; git rev-parse HEAD
printf 'requested head: '; git rev-parse "$head"
printf 'requested parent: '; git rev-parse "$parent"
printf 'merge-base: '; git merge-base "$head" "$parent"
printf 'ahead/behind: '; git rev-list --left-right --count "$parent...$head"
echo 'commits:'
git rev-list --reverse --oneline "$parent..$head"

echo '== Parent-relative changed paths =='
git diff --name-status "$parent" "$head"
git diff --stat "$parent" "$head"

echo '== Lifecycle source map =='
for f in \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java \
  src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java
do
  echo "-- $f"
  git show "$head:$f" > "/tmp/$(basename "$f")"
  ast-grep outline "/tmp/$(basename "$f")" --items all || true
done

echo '== Receipt validation and transitions =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceipt.java" | numbered

echo '== Ledger serialization, append, and replay =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionLedger.java" | numbered

echo '== Public contract and states =='
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionReceiptStore.java" | numbered
git show "$head:src/main/java/com/clearfolio/viewer/lifecycle/ArtifactDeletionState.java" | numbered

echo '== Exact-head ledger tests =='
git ls-tree -r --name-only "$head" src/test/java/com/clearfolio/viewer/lifecycle |
  rg 'ArtifactDeletion.*Test\.java$' |
  while IFS= read -r f; do
    echo "-- $f"
    git show "$head:$f" | numbered
  done

echo '== Documentation, changelog, and configuration =='
git diff --unified=30 "$parent" "$head" -- \
  CHANGELOG.md \
  docs/security/2026-08-06-durable-artifact-deletion-receipts.md \
  src/main/resources/application.yml

echo '== Parent explicit-tenant lookup contract, unchanged by this head =='
git show "$parent:src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTest.java" |
  rg -n -C 5 '(findByTenantAndContentHash|blank|null|findByContentHash)' || true
git diff --name-status "$parent" "$head" -- \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTest.java

echo '== Scope exclusions and integration references =='
git diff --name-only "$parent" "$head" |
  rg '(^|/)(controller|repository|worker|scheduler|workflow|artifact)(/|$)|(^|/)(pom\.xml|build\.gradle|build\.gradle\.kts|package\.json|Dockerfile)$|^\.github/|release|CHANGELOG' || true
git grep -n -E '(ArtifactDeletionReceiptStore|ArtifactDeletionLedger|ArtifactDeletionReceipt|ArtifactDeletionState)' \
  "$head" -- ':!src/main/java/com/clearfolio/viewer/lifecycle' ':!src/test/java/com/clearfolio/viewer/lifecycle' || true

Length of output: 122137


@seonghobae I reviewed exact head 8448fdd98d4a6c58f4e5407fcfcce903785abaa8 against parent d57f1aa8153aa092607a147c9587c683f009ea06.

I found one actionable finding.

  • [P1] Set up the append failure after ledger construction. In ArtifactDeletionLedgerCoverageTest.loadAndWriteIoFailuresAreReportedWithoutPartialSuccess, Line 238 creates parentFile as a regular file. Line 240 constructs ArtifactDeletionLedger at parentFile/ledger.log. Construction calls load(), and Files.newInputStream(...) fails before request(...) executes. The observable result is "artifact deletion ledger cannot be loaded", not the expected "artifact deletion ledger cannot be written". Therefore, the test does not exercise the append failure path or prove that the failed request remains invisible. Create an empty parent directory and construct the ledger for a missing child path. Then delete the empty directory, replace it with a regular file, call request(...), and assert the write error and absent receipt.

I also verified the following:

  • The merge base is the declared parent.
  • The head is two commits ahead and zero commits behind the parent.
  • The parent-relative diff changes exactly 11 ledger-scoped files.
  • Receipt identity includes request ID, tenant ID, permanently reserved job ID, lowercase SHA-256 checksum, audit correlation ID, and request time.
  • Exact duplicates return the existing receipt. Conflicting reuse of a job ID fails closed.
  • Lifecycle transitions are monotonic. Completion is terminal.
  • Attempt evidence requires matching count and timestamp presence. Requested and tombstoned states reject attempt evidence. Retry-pending and completion preserve prior attempt evidence. Only pending-to-failed increments the count.
  • Replay uses bounded strict UTF-8 decoding. It rejects malformed records, identity conflicts, illegal transitions, non-monotonic time, and non-empty unterminated tails.
  • Append writes a fixed LF delimiter and calls FileChannel.force(true) before it exposes the new receipt state.
  • The crash-tail test verifies LF output and fail-closed restart behavior after delimiter removal.
  • The public store interface documents a standalone and replaceable adapter boundary.
  • The parent test still requires findByTenantAndContentHash to fail closed for null or blank tenants. The explicit legacy findByContentHash lookup remains unchanged.
  • The ledger slice adds no endpoint, existing repository mutation, artifact-store mutation, worker, scheduler, workflow, dependency, version, or release change.
  • The ADR and changelog keep incomplete artifact cleanup open under #263.

I did not run tests because this inspection environment does not execute repository code. I did not infer CI, security, coverage, Javadoc, protected-branch, or independent approval status from the reported fuzz result. This PR remains Draft. The incomplete-cleanup finding remains open.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@seonghobae
seonghobae force-pushed the feat/durable-deletion-receipt-ledger branch from 36b192d to 8448fdd Compare August 6, 2026 11:42
@seonghobae
seonghobae force-pushed the feat/durable-deletion-receipt-ledger branch from 909c38d to 36b192d Compare August 6, 2026 11:49

Copy link
Copy Markdown
Collaborator Author

This mutable branch is superseded by two immutable, bounded Drafts:

The current #277 head and all of its checks/reviews are stale for those clean slices. Closing this PR unmerged prevents further automated commits from mixing issue #263 Slice B and Slice C. No approval, protection, or completion is inferred from this closure.

@seonghobae seonghobae closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant