Class AnnotationSetServiceImpl
- All Implemented Interfaces:
AnnotationSetService
AnnotationSetService implementation.
attach(Investigation, AnnotationSetRole, AnnotationSetSource, AgentCurationKind, String, String, String, String, Date, String, AnnotationSet) carries @AuditedConditional: an event is
emitted only on actual insert (the result.created flag), not
on the idempotent same-(role, runId) retry path. The audit
note carries the row id so the audit trail links to the row without
inlining the JSON payload.
-
Nested Class Summary
Nested classes/interfaces inherited from interface AnnotationSetService
AnnotationSetService.AttachedAnnotationSet, AnnotationSetService.RunProvenance -
Field Summary
Fields inherited from interface AnnotationSetService
PRE_COMMIT_SNAPSHOT_RUN_ID_PREFIX -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionattach(Investigation investigation, AnnotationSetRole role, AnnotationSetSource source, AgentCurationKind kind, String runId, String createdBy, String agentVersion, String model, Date ranAt, String payloadJson, AnnotationSet parent) Attach (or return existing) an annotation set to the given investigation.attach(Investigation investigation, AnnotationSetRole role, AnnotationSetSource source, AgentCurationKind kind, String runId, String createdBy, AnnotationSetService.RunProvenance runProvenance, String payloadJson, AnnotationSet parent) AsAnnotationSetService.attach(Investigation, AnnotationSetRole, AnnotationSetSource, AgentCurationKind, String, String, String, String, Date, String, AnnotationSet), but carrying the full run provenance rather than theagentVersion/model/ranAttriple.longcountByInvestigation(Investigation investigation, AnnotationSetRole roleFilter) countByRoleSince(Date since) longcountDistinctRunIdsSince(Date since, AnnotationSetRole roleFilter) longcountSince(Date since, AnnotationSetRole roleFilter) longcountSummaries(AnnotationSetRole roleFilter, AnnotationSetSource sourceFilter, String createdByFilter, AgentCurationKind kindFilter, String statusFilter, List<Long> investigationIds) booleanDelete the row with the given id.finalizeSet(Long id, String finalizedBy, String notes) StampfinalizedAt+finalizedByon the row, marking a DRAFT as "done editing" or a SNAPSHOT as the polished canonical view.findByInvestigation(Investigation investigation, AnnotationSetRole roleFilter) findByInvestigationAndRoleAndRunId(Investigation investigation, AnnotationSetRole role, String runId) findLatestByInvestigation(Investigation investigation, AnnotationSetRole roleFilter) findLatestCreatedAt(AnnotationSetRole roleFilter) findSummariesByInvestigation(Investigation investigation, AnnotationSetRole roleFilter) Thin metadata projection.listSummaries(AnnotationSetRole roleFilter, AnnotationSetSource sourceFilter, String createdByFilter, AgentCurationKind kindFilter, String statusFilter, List<Long> investigationIds, int offset, int limit, AnnotationSetDao.SummarySort sort, boolean descending) intrebindInvestigation(Investigation from, Investigation to) ClearfinalizedAt+finalizedBy+finalizedNoteson the row, reopening a finalized DRAFT or unblessing a polished SNAPSHOT.updateProvenance(Long id, AnnotationSetService.RunProvenance provenance) Correct the run-provenance envelope on an existing row in place, so a mis-stampedagentVersion/model/agentName/runSha/ranAtis fixed without the delete-and-recreate that mints a new id.updateStatus(AnnotationSet annotationSet, String status) Record where a proposal stands with its reviewer, and return the updated set.upsertDraft(Investigation investigation, String createdBy, String payloadJson, String parkedElements, AnnotationSet parent) Convenience overload for the common DRAFT-upsert path used by the curation-UI: ensures one DRAFT per(investigation, curator)by derivingrunIdas"draft-{createdBy}".
-
Constructor Details
-
AnnotationSetServiceImpl
-
-
Method Details
-
attach
@Transactional @AuditedConditional(value=AnnotationSetEvent.class, when="#result != null and #result.created and #result.annotationSet.role.name() != 'SNAPSHOT' and #result.annotationSet.role.name() != 'COMMIT'", messageSpel="'AnnotationSet#' + #result.annotationSet.id + ' role=' + #result.annotationSet.role.dbValue + ' source=' + #result.annotationSet.source.dbValue + (#result.annotationSet.kind != null ? ' kind=' + #result.annotationSet.kind.dbValue : '') + ' run=' + #result.annotationSet.runId + (#result.annotationSet.agentName != null ? ' agent=' + #result.annotationSet.agentName : '') + (#result.annotationSet.agentVersion != null ? ' version=' + #result.annotationSet.agentVersion : '') + (#result.annotationSet.model != null ? ' model=' + #result.annotationSet.model : '') + (#result.annotationSet.runSha != null ? ' sha=' + #result.annotationSet.runSha : '')") public 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) Description copied from interface:AnnotationSetServiceAttach (or return existing) an annotation set to the given investigation. TherunIdsemantic depends on role and is the caller's responsibility:PROPOSAL— pass the agent runner's unique id.DRAFT— pass"draft-{createdBy}"(or the service derives it if blank; seeAnnotationSetService.upsertDraft(Investigation, String, String, String, AnnotationSet)).SNAPSHOT— pass a generated UUID, or let the service generate one whenrunIdis null/blank.
- Specified by:
attachin interfaceAnnotationSetService- Returns:
- the persisted row plus a flag noting whether it was created (true) or returned as existing (false).
-
attach
@Transactional @AuditedConditional(value=AnnotationSetEvent.class, when="#result != null and #result.created and #result.annotationSet.role.name() != 'SNAPSHOT' and #result.annotationSet.role.name() != 'COMMIT'", messageSpel="'AnnotationSet#' + #result.annotationSet.id + ' role=' + #result.annotationSet.role.dbValue + ' source=' + #result.annotationSet.source.dbValue + (#result.annotationSet.kind != null ? ' kind=' + #result.annotationSet.kind.dbValue : '') + ' run=' + #result.annotationSet.runId + (#result.annotationSet.agentName != null ? ' agent=' + #result.annotationSet.agentName : '') + (#result.annotationSet.agentVersion != null ? ' version=' + #result.annotationSet.agentVersion : '') + (#result.annotationSet.model != null ? ' model=' + #result.annotationSet.model : '') + (#result.annotationSet.runSha != null ? ' sha=' + #result.annotationSet.runSha : '')") public 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) Description copied from interface:AnnotationSetServiceAsAnnotationSetService.attach(Investigation, AnnotationSetRole, AnnotationSetSource, AgentCurationKind, String, String, String, String, Date, String, AnnotationSet), but carrying the full run provenance rather than theagentVersion/model/ranAttriple.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.RunProvenancemakes that impossible and leaves the existing callers untouched.- Specified by:
attachin interfaceAnnotationSetService
-
upsertDraft
@Transactional public AnnotationSet upsertDraft(Investigation investigation, String createdBy, String payloadJson, @Nullable String parkedElements, @Nullable AnnotationSet parent) Description copied from interface:AnnotationSetServiceConvenience overload for the common DRAFT-upsert path used by the curation-UI: ensures one DRAFT per(investigation, curator)by derivingrunIdas"draft-{createdBy}". If a row already exists, itspayloadJson/parkedElements/updatedAtare updated in place; otherwise a new row is created.- Specified by:
upsertDraftin interfaceAnnotationSetService- Parameters:
parent- optionalPROPOSALthis draft was seeded from (forms the lineage edge for diff-derived dispositions).
-
findByInvestigation
@Transactional(readOnly=true) public List<AnnotationSet> findByInvestigation(Investigation investigation, @Nullable AnnotationSetRole roleFilter) - Specified by:
findByInvestigationin interfaceAnnotationSetService- Returns:
- all sets attached to the given investigation matching the role filter (or all roles if null), newest first.
-
findSummariesByInvestigation
@Transactional(readOnly=true) public List<AnnotationSetSummaryValueObject> findSummariesByInvestigation(Investigation investigation, @Nullable AnnotationSetRole roleFilter) Description copied from interface:AnnotationSetServiceThin metadata projection.- Specified by:
findSummariesByInvestigationin interfaceAnnotationSetService
-
findLatestByInvestigation
@Nullable @Transactional(readOnly=true) public AnnotationSet findLatestByInvestigation(Investigation investigation, @Nullable AnnotationSetRole roleFilter) - Specified by:
findLatestByInvestigationin interfaceAnnotationSetService
-
load
- Specified by:
loadin interfaceAnnotationSetService
-
findByInvestigationAndRoleAndRunId
@Nullable @Transactional(readOnly=true) public AnnotationSet findByInvestigationAndRoleAndRunId(Investigation investigation, AnnotationSetRole role, String runId) - Specified by:
findByInvestigationAndRoleAndRunIdin interfaceAnnotationSetService
-
countByInvestigation
@Transactional(readOnly=true) public long countByInvestigation(Investigation investigation, @Nullable AnnotationSetRole roleFilter) - Specified by:
countByInvestigationin interfaceAnnotationSetService
-
rebindInvestigation
- Specified by:
rebindInvestigationin interfaceAnnotationSetService
-
listSummaries
@Transactional(readOnly=true) public 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) - Specified by:
listSummariesin interfaceAnnotationSetService
-
countSummaries
@Transactional(readOnly=true) public long countSummaries(@Nullable AnnotationSetRole roleFilter, @Nullable AnnotationSetSource sourceFilter, @Nullable String createdByFilter, @Nullable AgentCurationKind kindFilter, @Nullable String statusFilter, @Nullable List<Long> investigationIds) - Specified by:
countSummariesin interfaceAnnotationSetService
-
finalizeSet
@Nullable @Transactional public AnnotationSet finalizeSet(Long id, @Nullable String finalizedBy, @Nullable String notes) Description copied from interface:AnnotationSetServiceStampfinalizedAt+finalizedByon 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).- Specified by:
finalizeSetin interfaceAnnotationSetService- Parameters:
notes- the curator's closing note, ornull. 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 asnull.🛑 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
Description copied from interface:AnnotationSetServiceClearfinalizedAt+finalizedBy+finalizedNoteson 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.
- Specified by:
reopenSetin interfaceAnnotationSetService
-
updateProvenance
@Nullable @Transactional public AnnotationSet updateProvenance(Long id, AnnotationSetService.RunProvenance provenance) Description copied from interface:AnnotationSetServiceCorrect the run-provenance envelope on an existing row in place, so a mis-stampedagentVersion/model/agentName/runSha/ranAtis 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.
ranAthas no blank form, so it can be corrected but not cleared.- Specified by:
updateProvenancein interfaceAnnotationSetService- Returns:
- the updated row, or null if no row has that id
-
delete
Description copied from interface:AnnotationSetServiceDelete 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 byON DELETE SET NULL: descendants survive, theirparentlink is cleared.- Specified by:
deletein interfaceAnnotationSetService
-
countSince
@Transactional(readOnly=true) public long countSince(@Nullable Date since, @Nullable AnnotationSetRole roleFilter) - Specified by:
countSincein interfaceAnnotationSetService
-
countByRoleSince
@Transactional(readOnly=true) public Map<AnnotationSetRole, Long> countByRoleSince(@Nullable Date since) - Specified by:
countByRoleSincein interfaceAnnotationSetService
-
countDistinctRunIdsSince
@Transactional(readOnly=true) public long countDistinctRunIdsSince(@Nullable Date since, @Nullable AnnotationSetRole roleFilter) - Specified by:
countDistinctRunIdsSincein interfaceAnnotationSetService
-
findLatestCreatedAt
@Nullable @Transactional(readOnly=true) public Date findLatestCreatedAt(@Nullable AnnotationSetRole roleFilter) - Specified by:
findLatestCreatedAtin interfaceAnnotationSetService
-
updateStatus
Description copied from interface:AnnotationSetServiceRecord 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 | rejectedare 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.- Specified by:
updateStatusin interfaceAnnotationSetService- Parameters:
annotationSet- the set to rule onstatus- the status, already trimmed and lowercased by the caller- Returns:
- the updated set
-