Conversation
Port `model delete`, `workflow delete`, `vault put`, and `data gc` commands to the libswamp event stream architecture (batch 5 partial of #739). Each command uses a two-phase pattern: 1. Preview function gathers what will be affected (read-only) 2. CLI prompts for confirmation using preview data 3. Execute generator performs the mutation, yields typed events Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Review: refactor confirmation-guarded mutations to libswamp pattern
Clean, well-structured refactor. The three-phase pattern (preview → prompt → execute) is consistent and the dependency injection via *Deps interfaces makes the generators testable. Architecture aligns well with DDD application service patterns — generators live in the application/libswamp layer and orchestrate domain objects.
No Blocking Issues
Suggestions
-
Missing renderer tests — The old
src/presentation/output/*_test.tsfiles were deleted but no replacement tests were created for the four new renderers (data_gc.ts,model_delete.ts,vault_put.ts,workflow_delete.ts). Every other renderer insrc/presentation/renderers/has a corresponding_test.ts. The generator tests cover the core logic well, but the renderers have enough branching (log vs JSON mode, error handling) that tests would be valuable. Not blocking since the old tests' coverage is partially subsumed by the generator tests, and this is a refactor with identical output behavior. -
workflowDeletegenerator doesn't re-validate the extension-only guard — The PR description says "generators re-validate preconditions… generators are self-contained."modelDeletedoes re-check workflow refs and data artifacts, butworkflowDeletedoesn't re-checkpathExists(the extension-only guard). A direct caller bypassing the preview could delete an extension-only workflow. Consider adding the guard to the generator for consistency with the stated design. -
ModelDeleteDatadroppedresourcePath— The oldModelDeleteJsonOutputconditionally includedresourcePathin the JSON output. The new type and renderer omit it entirely. This is probably intentional (it was alwaysundefined/falsein practice), but worth confirming there are no downstream consumers expecting this field. -
Error conversion pattern in CLI could be more precise — The
catchblocks use"code" in (error as Record<string, unknown>)to detectSwampError. This would match any error object that happens to have acodeproperty (e.g., Node/Deno system errors havecode). A more precise check might use a type guard or check for the specificSwampErrorshape. Low risk in practice since the preview functions are well-defined. -
promptConfirmationis duplicated acrossdata_gc.ts,model_delete.ts,vault_put.ts, andworkflow_delete.ts. Consider extracting to a shared CLI utility. Not blocking since this was pre-existing duplication.
Overall this is a solid refactor that cleanly separates concerns and improves testability. LGTM.
🤖 Generated with Claude Code
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity findings.
Medium
-
src/libswamp/workflows/delete.ts:122-150—workflowDeletegenerator omits the extension-only guard, contradicting the stated "self-contained generator" design.The PR description explicitly states: "Generators are self-contained. A web UI adapter or API consumer might call the generator directly…" and "The library contract doesn't assume the preview ran first."
modelDeletere-validates all preconditions (workflow refs, data artifact checks). ButworkflowDeletedoes NOT re-check the extension-only guard (pathExists). A non-CLI caller invokingworkflowDeletedirectly (without callingworkflowDeletePreviewfirst) could delete an extension-only workflow's runs and evaluated data beforedeleteWorkfloweither fails or succeeds in an unintended way — leaving a partially-deleted state.Breaking scenario: A future web UI adapter calls
workflowDelete("ext-workflow")without callingworkflowDeletePreviewfirst. The generator deletes runs, deletes evaluated workflow, then callsdeleteWorkflowwhich may throw (if the path doesn't exist on disk) — but runs are already gone.Suggested fix: Add the
pathExistscheck inside theworkflowDeletegenerator body, yielding an error event if the workflow is extension-only, matching the pattern used bymodelDeletefor its preconditions. -
src/cli/commands/model_delete.ts:68-72,vault_put.ts:170-174,workflow_delete.ts:67-71— Error discrimination via"code" in erroris fragile.if ("code" in (error as Record<string, unknown>)) { throw new UserError((error as { message: string }).message); }
This catches ANY error object with a
codeproperty — not justSwampError. Node-compatible system errors (e.g.,ERR_*codes),DOMException, or any third-party error with a.codefield would be silently swallowed into aUserError, losing the original stack trace and potentially exposing internal error messages to the user.Breaking scenario: A
Deno.errors.PermissionDeniedwrapping a Node-compat API that sets.code = "EACCES"would be caught here and re-thrown as a plainUserError("Permission denied")instead of propagating as a proper system error.Suggested fix: Use a dedicated type guard, e.g., check for a known
SwampErrorcode set or add a discriminant field like_swampError: trueto theSwampErrorinterface.
Low
-
src/libswamp/vaults/put.ts:91-94—VaultPutInput.overwrittenis caller-supplied, not re-verified by the generator. The generator trusts this display-only flag from the caller. A non-CLI consumer passingoverwritten: falsewhen the secret actually exists will produce inaccurate output metadata. The actualputSecretoperation is unaffected, so this is cosmetic only. -
src/libswamp/models/delete.ts:148-165— Partial deletion on infrastructure failure. IfdeleteOutputsucceeds for some outputs butdeleteDataordeleteDefinitionthrows, the model is left partially deleted with no rollback. This matches pre-refactor behavior and filesystem errors mid-loop are rare, so flagging only for awareness. -
TOCTOU between preview and execute across all commands. State can change between the preview phase and the execute phase (e.g., new workflow references added, secrets created/deleted, new data artifacts). This is mitigated by design:
modelDeletere-validates preconditions,dataGcdelegates to the service which handles this internally, andvaultPut's put is an upsert. The only gap isworkflowDelete(see Medium #1 above).
Verdict
PASS — This is a clean, well-structured refactor. The two medium findings are real but non-blocking: the workflow delete generator inconsistency is a design gap for future callers (not current CLI behavior), and the error discrimination pattern is fragile but works in practice with Deno's current error types. The code is consistent across all four commands, tests are thorough, and the preview/prompt/execute separation is sound.
Summary
Ports 4 confirmation-guarded mutation commands to the libswamp event stream architecture, partially completing batch 5 of #739.
model delete— validates no workflow refs, checks data artifacts, prompts, deletes outputs + data + definitionworkflow delete— checks extension-only guard, counts runs, prompts, deletes runs + evaluated + workflowvault put— resolves key/value from CLI/stdin/TTY, checks overwrite, prompts, stores secret + publishes eventdata gc— finds expired data, shows preview, prompts, executes garbage collectionNot included in this PR:
extension pull(~1200 lines, recursive dependency installation — needs dedicated migration) andextension rm(depends onextension pullexports).Two-phase confirmation pattern
Per the issue's architectural decision: "Confirmation prompts live in the CLI layer before calling the generator."
Each command follows a three-step flow:
The preview is a plain async function (not a generator) because it returns a single result — no streaming needed. It throws
SwampErroron hard blocks (model not found, extension-only workflow).The generator only runs after confirmation. If the user cancels, the CLI renders cancellation directly without invoking the generator.
Architecture trade-off: duplicated validation in generators
The generators re-validate preconditions (e.g.,
modelDeletere-checks workflow references and data artifacts) even though the CLI already checked them via the preview. This is intentional:User impact
None. Output is identical in both
logandjsonmodes — same log lines, same JSON shapes, same confirmation prompts. This is a purely internal refactor.Files
src/libswamp/{models/delete,workflows/delete,vaults/put,data/gc}.ts*_test.tsfor each generatorsrc/presentation/renderers/{model_delete,workflow_delete,vault_put,data_gc}.tsrenderXxxCancelled()for prompt-declined pathsrc/cli/commands/{model_delete,workflow_delete,vault_put,data_gc}.tssrc/libswamp/mod.tssrc/presentation/output/files + testsTest plan
deno check— cleandeno lint— cleandeno fmt— cleanPartially addresses batch 5 of #739 (4 of 6 commands).
🤖 Generated with Claude Code