Interface AnnotationSetService

All Known Implementing Classes:
AnnotationSetServiceImpl

public interface AnnotationSetService
Service surface for AnnotationSet rows. Drives the /datasets/{id}/annotation-sets + /annotation-sets/{id} REST endpoints and the /preboarded/{id}/annotation-sets pre-load surface.

attach(Investigation, AnnotationSetRole, AnnotationSetSource, AgentCurationKind, String, String, String, String, Date, String, AnnotationSet) is idempotent on (investigation, role, runId): a retry with the same triple returns the existing row instead of creating a duplicate. The REST layer reports the existing row as 200 OK rather than 201 Created in that case.

  • Field Details

    • PRE_COMMIT_SNAPSHOT_RUN_ID_PREFIX

      static final String PRE_COMMIT_SNAPSHOT_RUN_ID_PREFIX
      Run-id prefix on a SNAPSHOT taken by a curation commit to keep the state it displaced, as opposed to one a caller asked for through POST /datasets/{id}/annotation-sets/snapshot. Both are ordinary snapshots and restore the same way; the prefix is what tells them apart in a list.

      The rest of the id is a UUID, as for any snapshot — a commit-taken backup names no run of its own.

      See Also:
  • Method Details

    • attach

      AnnotationSetService.AttachedAnnotationSet attach(Investigation investigation, AnnotationSetRole role, AnnotationSetSource source, @Nullable AgentCurationKind kind, @Nullable String runId, @Nullable String createdBy, @Nullable String agentVersion, @Nullable String model, @Nullable Date ranAt, @Nullable String payloadJson, @Nullable AnnotationSet parent)
      Attach (or return existing) an annotation set to the given investigation. The runId semantic depends on role and is the caller's responsibility:
      Returns:
      the persisted row plus a flag noting whether it was created (true) or returned as existing (false).
    • attach

      AnnotationSetService.AttachedAnnotationSet attach(Investigation investigation, AnnotationSetRole role, AnnotationSetSource source, @Nullable AgentCurationKind kind, @Nullable String runId, @Nullable String createdBy, @Nullable AnnotationSetService.RunProvenance runProvenance, @Nullable String payloadJson, @Nullable AnnotationSet parent)
      As attach(Investigation, AnnotationSetRole, AnnotationSetSource, AgentCurationKind, String, String, String, String, Date, String, AnnotationSet), but carrying the full run provenance rather than the agentVersion / model / ranAt triple.

      An overload rather than two more parameters on the method above: that signature already takes eleven arguments, six of which would then be adjacent nullable strings meaning different things (runId, createdBy, agentVersion, model, runSha, agentName). Transposing two of those compiles silently and mis-files provenance in a way nothing would catch. AnnotationSetService.RunProvenance makes that impossible and leaves the existing callers untouched.

    • upsertDraft

      AnnotationSet upsertDraft(Investigation investigation, String createdBy, String payloadJson, @Nullable String parkedElements, @Nullable AnnotationSet parent)
      Convenience overload for the common DRAFT-upsert path used by the curation-UI: ensures one DRAFT per (investigation, curator) by deriving runId as "draft-{createdBy}". If a row already exists, its payloadJson / parkedElements / updatedAt are updated in place; otherwise a new row is created.
      Parameters:
      parent - optional PROPOSAL this draft was seeded from (forms the lineage edge for diff-derived dispositions).
    • findByInvestigation

      List<AnnotationSet> findByInvestigation(Investigation investigation, @Nullable AnnotationSetRole roleFilter)
      Returns:
      all sets attached to the given investigation matching the role filter (or all roles if null), newest first.
    • findSummariesByInvestigation

      List<AnnotationSetSummaryValueObject> findSummariesByInvestigation(Investigation investigation, @Nullable AnnotationSetRole roleFilter)
      Thin metadata projection.
    • findLatestByInvestigation

      @Nullable AnnotationSet findLatestByInvestigation(Investigation investigation, @Nullable AnnotationSetRole roleFilter)
    • load

      @Nullable AnnotationSet load(Long id)
    • findByInvestigationAndRoleAndRunId

      @Nullable AnnotationSet findByInvestigationAndRoleAndRunId(Investigation investigation, AnnotationSetRole role, String runId)
    • countByInvestigation

      long countByInvestigation(Investigation investigation, @Nullable AnnotationSetRole roleFilter)
    • rebindInvestigation

      int rebindInvestigation(Investigation from, Investigation to)
    • listSummaries

      List<AnnotationSetSummaryValueObject> listSummaries(@Nullable AnnotationSetRole roleFilter, @Nullable AnnotationSetSource sourceFilter, @Nullable String createdByFilter, @Nullable AgentCurationKind kindFilter, @Nullable String statusFilter, @Nullable List<Long> investigationIds, int offset, int limit, @Nullable AnnotationSetDao.SummarySort sort, boolean descending)
    • countSummaries

      long countSummaries(@Nullable AnnotationSetRole roleFilter, @Nullable AnnotationSetSource sourceFilter, @Nullable String createdByFilter, @Nullable AgentCurationKind kindFilter, @Nullable String statusFilter, @Nullable List<Long> investigationIds)
    • finalizeSet

      @Nullable AnnotationSet finalizeSet(Long id, @Nullable String finalizedBy, @Nullable String notes)
      Stamp finalizedAt + finalizedBy on the row, marking a DRAFT as "done editing" or a SNAPSHOT as the polished canonical view. Idempotent: a row already finalized returns unchanged (no re-stamp).
      Parameters:
      notes - the curator's closing note, or null. Trimmed, and truncated to fit rather than rejected — losing the tail of an explanation is a smaller harm than refusing a closure the curator has already decided on. A blank note is stored as null.

      🛑 The one thing this is NOT idempotent about. A set that is already finalized is still re-stamped with a non-blank notes, because dropping the sentence and answering 200 is the exact failure the parameter exists to fix; a caller cannot tell that from a successful write. Everything else about the row stays as it was.

    • reopenSet

      @Nullable AnnotationSet reopenSet(Long id)
      Clear finalizedAt + finalizedBy + finalizedNotes on the row, reopening a finalized DRAFT or unblessing a polished SNAPSHOT. Idempotent: a row already not finalized returns unchanged.

      The note goes with the closure it explains. Carrying it across a reopen would attach one closure's words to the next one, and the UI pre-fills the re-close box from the value it read BEFORE reopening.

    • updateProvenance

      @Nullable AnnotationSet updateProvenance(Long id, AnnotationSetService.RunProvenance provenance)
      Correct the run-provenance envelope on an existing row in place, so a mis-stamped agentVersion / model / agentName / runSha / ranAt is fixed without the delete-and-recreate that mints a new id. Only the envelope moves; the set's content (payloadJson, parkedElements), its identity (role, source, runId, investigation, parent) and its finalized status are untouched.

      Per field: null leaves the stored value alone, and a blank string clears it. ranAt has no blank form, so it can be corrected but not cleared.

      Returns:
      the updated row, or null if no row has that id
    • delete

      boolean delete(Long id)
      Delete the row with the given id. Returns true if a row was removed, false if no such row existed. Cascades on parent edges are governed by ON DELETE SET NULL: descendants survive, their parent link is cleared.
    • countSince

      long countSince(@Nullable Date since, @Nullable AnnotationSetRole roleFilter)
    • countByRoleSince

      Map<AnnotationSetRole, Long> countByRoleSince(@Nullable Date since)
    • countDistinctRunIdsSince

      long countDistinctRunIdsSince(@Nullable Date since, @Nullable AnnotationSetRole roleFilter)
    • findLatestCreatedAt

      @Nullable Date findLatestCreatedAt(@Nullable AnnotationSetRole roleFilter)
    • updateStatus

      AnnotationSet updateStatus(AnnotationSet annotationSet, String status)
      Record where a proposal stands with its reviewer, and return the updated set.

      🛑 The value is stored as given. There is deliberately no vocabulary check here — Paul, 2026-09-04: "don't lock us into any kind of enums. if we settle down on this we might formalize it." pending | needs_changes | accepted | rejected are the values in use, not the values permitted, and a caller sending a fifth gets it stored rather than rejected. The only gate is structural: non-blank, and short enough for the column.

      Parameters:
      annotationSet - the set to rule on
      status - the status, already trimmed and lowercased by the caller
      Returns:
      the updated set