# Fileserver Production Capability Foundation Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the list-materializing CSV demo boundary with the Phase 1 framework-free publication contract and a bounded, staged local CSV R1 provider without claiming crash-safe R2 guarantees. **Architecture:** `application-core` owns typed publication requests, rows, cells, producer/sink callbacks, opaque references, and receipts. `adapter:outbound:fileserver` owns CSV encoding, spreadsheet-formula mitigation, staging, digest/count limits, and local atomic publication. The legacy `FileExportPort` remains temporarily for compatibility and is explicitly documented as deprecated R0/R1 behavior. **Tech Stack:** Java 21, JUnit 5, AssertJ, Spring Boot configuration properties, JDK NIO filesystem and SHA-256. --- ### Task 1: Add the framework-free publication contract **Files:** - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublicationPort.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublishRequest.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublishOperationId.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FileDestinationId.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/LogicalFileName.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/SourceRevision.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/ExportSchema.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/TabularCell.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/TabularRow.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/TabularRowProducer.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/TabularRowSink.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublishReceipt.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/PublishedFileReference.java` - Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FileVersion.java` - Test: `src/application-core/src/test/java/dev/caskeleton/application/filepublication/FilePublicationContractTest.java` - [ ] **Step 1: Write the failing contract test** ```java @Test void requestRejectsPathLikeLogicalNamesAndSchemaRejectsDuplicateColumns() { assertThatThrownBy(() -> new LogicalFileName("../report.csv")) .isInstanceOf(IllegalArgumentException.class); assertThatThrownBy( () -> new ExportSchema( "worklog-v1", 1, List.of( new ExportSchema.Column( "id", ExportSchema.CellType.INTEGER, false, ExportSchema.FormulaPolicy.REJECT, 64), new ExportSchema.Column( "id", ExportSchema.CellType.TEXT, false, ExportSchema.FormulaPolicy.MITIGATE, 128)))) .isInstanceOf(IllegalArgumentException.class); } ``` - [ ] **Step 2: Verify RED** Run: `cd src && ./gradlew :application-core:test --tests '*FilePublicationContractTest' --console=plain` Expected: compilation failure because the `filepublication` contract does not exist. - [ ] **Step 3: Implement immutable validated values** The contract must expose this shape and no `Path`, `File`, stream, Spring, or provider type: ```java public interface FilePublicationPort { FilePublishReceipt publish(FilePublishRequest request, TabularRowProducer producer); } @FunctionalInterface public interface TabularRowProducer { void produce(TabularRowSink sink); } public interface TabularRowSink { void write(TabularRow row); void checkpoint(); } ``` `TabularCell` is a sealed interface with nested records for text, integer, decimal, boolean, date, instant, and null. `ExportSchema` owns ordered columns, cell type, nullability, formula policy, and per-cell byte bounds. Records reject null/blank IDs, path separators in `LogicalFileName`, duplicate column names, empty schemas, and non-positive limits. - [ ] **Step 4: Verify GREEN** Run: `cd src && ./gradlew :application-core:test --tests '*FilePublicationContractTest' --console=plain` Expected: PASS. ### Task 2: Add streaming CSV encoding and staged local publication **Files:** - Create: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/StreamingCsvEncoder.java` - Create: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationAdapter.java` - Create: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationPolicy.java` - Test: `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationAdapterTest.java` - [ ] **Step 1: Write the failing streaming publication tests** ```java @Test void publishesRowsThroughTheSinkAndReturnsAnOpaqueReceipt() { AtomicInteger calls = new AtomicInteger(); FilePublishReceipt receipt = adapter.publish( request(), sink -> { calls.incrementAndGet(); sink.write(new TabularRow(List.of(new IntegerCell(1), new TextCell("=cmd")))); }); assertThat(calls).hasValue(1); assertThat(receipt.reference().value()).doesNotContain(tempDir.toString()); assertThat(Files.readString(publishedFile(receipt), UTF_8)).contains("1,'=cmd"); } @Test void abortsBeforeFinalPublicationWhenTheByteLimitIsExceeded() { assertThatThrownBy( () -> adapter.publish(request(), sink -> sink.write(oversizedRow()))) .isInstanceOf(FilePublicationException.class); assertThat(finalArtifacts()).isEmpty(); } ``` - [ ] **Step 2: Verify RED** Run: `cd src && ./gradlew :adapter:outbound:fileserver:test --tests '*LocalFilePublicationAdapterTest' --console=plain` Expected: compilation failure because the staged provider does not exist. - [ ] **Step 3: Implement the minimum staged provider** `LocalFilePublicationPolicy` validates a fixed destination ID, base directory, maximum rows, maximum encoded bytes, and the only initial format profile `csv-rfc4180-v1`. `LocalFilePublicationAdapter` must: ```text validate request/schema before producer invocation create a private .staging directory exclusive-create an operation-scoped .part file write header and each row directly through StreamingCsvEncoder enforce schema/cell/row/byte limits at each sink call prefix dangerous spreadsheet text with a single quote when policy is MITIGATE compute SHA-256 and counts while writing flush and FileChannel.force(true) move staging to the final operation-scoped file with ATOMIC_MOVE delete staging on pre-publish failure return an opaque reference and never an absolute path ``` The first release is labelled local R1. Existing final artifacts cause a typed conflict; durable operation journals, crash reconciliation, replace semantics, and SFTP/NFS remain unimplemented and must not be advertised. - [ ] **Step 4: Verify GREEN** Run: `cd src && ./gradlew :adapter:outbound:fileserver:test --tests '*LocalFilePublicationAdapterTest' --console=plain` Expected: PASS. ### Task 3: Add opt-in R1 composition and truthful documentation **Files:** - Modify: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportProperties.java` - Modify: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportConfig.java` - Create: `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationConfigTest.java` - Modify: `src/adapter/outbound/fileserver/README.md` - Modify: `src/adapter/outbound/fileserver/CLAUDE.md` - [ ] **Step 1: Write the failing composition test** ```java @Test void disabledConfigurationCreatesNoPublicationPort() { contextRunner .withUserConfiguration(FileExportConfig.class) .run(context -> assertThat(context).doesNotHaveBean(FilePublicationPort.class)); } @Test void enabledConfigurationCreatesExactlyOneLocalR1PublicationPort() { contextRunner .withUserConfiguration(FileExportConfig.class) .withPropertyValues( "ca-skeleton.fileserver.enabled=true", "ca-skeleton.fileserver.destination-id=local-export", "ca-skeleton.fileserver.base-directory=" + tempDir) .run(context -> assertThat(context).hasSingleBean(FilePublicationPort.class)); } ``` - [ ] **Step 2: Verify RED** Run: `cd src && ./gradlew :adapter:outbound:fileserver:test --tests '*FilePublicationConfigTest' --console=plain` Expected: FAIL because the new port is not composed. - [ ] **Step 3: Wire only the local R1 provider** Add validated destination ID, row limit, byte limit, and format-profile settings. Contribute `FilePublicationPort` only when explicitly enabled. Keep `FileExportPort` as a deprecated compatibility bean and document that it materializes caller rows and is not R2 evidence. - [ ] **Step 4: Verify module and architecture gates** Run: ```bash cd src ./gradlew :application-core:test :adapter:outbound:fileserver:check --console=plain ./gradlew verifyCleanArchitectureDependencies --console=plain ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain ``` Expected: all commands PASS. ### Task 4: Record the unfinished R2 boundary **Files:** - Modify: `docs/superpowers/specs/2026-07-26-fileserver-production-capability-design.md` - [ ] **Step 1: Update implementation status without weakening completion criteria** Record Phase 0–1/local R1 foundation as implemented. Keep Phase 2 durable journal/reconciliation, Phase 3 operations, Phase 4 SFTP, Phase 5 NFS/HA/bootstrap, and Phase 6 optional operations marked unimplemented. The document must still say that local R1 is not Fileserver R2. - [ ] **Step 2: Verify documentation structure** Run: `rg -n 'R1|R2|구현 상태|미구현' docs/superpowers/specs/2026-07-26-fileserver-production-capability-design.md` Expected: explicit R1 implementation and remaining R2 gaps are both present.