feat(vaults): annotation UX improvements#1439
Conversation
…clear validation, remove-label, secretExists optimization Addresses five UX improvements for vault annotations identified in swamp-club#418: 1. Rename --note to --notes for consistency with the data model field name 2. Include resulting annotation state in vault annotate --json output 3. Validate --clear mutual exclusivity with --url/--notes/--label/--remove-label 4. Add --remove-label flag for single-label deletion without clearing all annotations 5. Replace secretExists svc.get() with svc.list() membership check to avoid unnecessary AES-GCM decryption in annotate, inspect, and put deps Closes swamp-club#418 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
No
--remove-labelexample in--help(src/cli/commands/vault_annotate.ts): The command has.example()calls for URL+notes, labels, and--clear, but nothing for the new--remove-labelflag. A new flag without a help example is harder to discover. Suggested addition:.example("Remove a label", "swamp vault annotate my-vault API_KEY --remove-label team") -
Description body doesn't mention
--remove-label(src/cli/commands/vault_annotate.ts, line 70): The description says "Use--clearto remove all annotations" but doesn't mention that individual labels can now be removed with--remove-label. A user skimming the description might miss this. Suggested addition to the description: "Use--remove-labelto remove individual labels, or--clearto remove all annotations." -
Log-mode output doesn't name which labels were removed: When
--remove-label teamis used, the output isAnnotated API_KEY in vault my-vault (fields: labels). This is consistent with how other field updates are reported, but it doesn't distinguish between "added a label" and "removed label 'team'". Minor—the current behavior is internally consistent, so only worth changing if the other field updates also get more specific. -
--note→--notesis a breaking change: Existing user scripts that pass--notewill silently fail (Cliffy will likely error on unknown flag). The rename is correct—it aligns with the data model—but a note in the release changelog would help users who hit this. Not a CLI change needed, just a release comms callout.
Verdict
PASS — no blocking issues. The five UX improvements (flag rename, JSON annotation output, --clear validation, --remove-label, secretExists optimization) are all clean and correct. Error messages are clear and actionable. JSON output now includes the full annotation state, which eliminates the follow-up vault inspect call.
There was a problem hiding this comment.
Code Review
Clean, well-tested set of annotation UX improvements. DDD patterns are correctly applied throughout.
Blocking Issues
None.
Suggestions
-
Re-export
VaultAnnotationDatafromsrc/libswamp/mod.ts: The newannotation: VaultAnnotationData | nullfield on the publicVaultAnnotateDatainterface references a domain type that isn't re-exported through the libswamp barrel. Consumers that need to reference this type by name (e.g., for typed variables or function signatures) would have to import from the domain layer directly, violating the import boundary. TypeScript structural typing makes this work today, but an explicit re-export would be more robust. -
secretExistsviasvc.list()+.includes(): The switch fromsvc.get()(AES-GCM decryption) tosvc.list()(key enumeration) is a good improvement. For very large vaults, a dedicatedsvc.has(vaultName, key)method could avoid allocating the full key array — but that's a future optimization, not something this PR needs to address.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
Missing
--clearmutual exclusivity validation in libswamp layer (src/libswamp/vaults/annotate.ts:203-217): The CLI layer (vault_annotate.ts:123-131) correctly validates that--clearcannot be combined with--url/--notes/--label/--remove-label, but thevaultAnnotategenerator function has no such check. Ifinput.clearistruealongsideurl: "foo", the generator silently enters theif (input.clear)branch at line 217 and ignores the other fields. Any future caller of the libswamp API that bypasses the CLI would not get the validation error. This is a defense-in-depth concern — the CLI catches it today, but the library should enforce its own invariants. -
removeLabelson nonexistent annotation persists a vacuous annotation (src/libswamp/vaults/annotate.ts:251-268): If a user runsswamp vault annotate my-vault KEY --remove-label teamon a secret that has no existing annotation, the code creates a new emptyVaultAnnotation(viaVaultAnnotation.create({})), appliesremoveLabels(a no-op on empty labels), and then persists the empty annotation viaputAnnotation. The result is a saved annotation with only anupdatedAtfield —isEmpty()returnstruebut it occupies storage. Consider either skipping the write when the result is empty, or documenting that this is expected behavior.
Low
-
svc.list()performance characteristic change (annotate.ts:110,inspect.ts:77,put.ts:103): The oldsecretExistsusedsvc.get()(single-key decrypt), replaced withsvc.list()(fetch all key names). This avoids AES-GCM decryption per the stated goal, but trades it for O(n) key listing on backends with many secrets. Forput.tsspecifically,secretExistsis called before every store operation — if a vault has thousands of keys, listing them all for a membership check could be slower than the old decrypt-one approach. Fine for current scale; worth keeping in mind if vault sizes grow. -
--label X=Y --remove-label Xordering is implicit (annotate.ts:252-265): Labels are added viamerge()first, thenremoveLabelsis applied after. This means--label team=infra --remove-label teamadds then removes — removal wins. The test at line 385 documents this behavior explicitly, which is good. The ordering is deterministic and reasonable, but could surprise users who expect the flags to conflict.
Verdict
PASS — Clean implementation with good test coverage (7 new tests covering the new paths). The new features are well-separated between domain, library, and CLI layers. The --clear validation gap and empty-annotation persistence are minor defense-in-depth concerns, not correctness bugs.
Summary
Addresses five UX improvements for vault annotations identified in swamp-club#418:
--note→--notes: Rename flag to match the data model field name (notes)vault annotate --jsonnow includes the resulting annotation state, eliminating the need for a follow-upvault inspectcall--clearvalidation: Error when--clearis combined with--url/--notes/--label/--remove-label(previously silently discarded)--remove-label <key>: New repeatable flag for removing individual labels without clearing all annotationssecretExistsoptimization: Replacesvc.get()(full AES-GCM decryption) withsvc.list()membership check in annotate, inspect, and put depsCloses swamp-club#418
Test plan
deno check— type checking passesdeno lint— no lint errorsdeno fmt --check— formatting correctdeno run test— all 6169 tests pass (37 vault-specific tests including 7 new ones)deno run compile— binary compiles successfullynpx tessl skill review— swamp-vault skill passes at 94%🤖 Generated with Claude Code