work

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 45 Imported by: 0

Documentation

Overview

Package work holds the work management use cases: containers and the items inside them.

Index

Constants

View Source
const (
	AddCommentName = "AddComment"

	// CommentCreatedAction is the audit code. Stable: an auditor filters on it and a SIEM rule
	// matches on it (audit.md §2).
	CommentCreatedAction audit.Action = "comment.created"
)
View Source
const (
	AddLabelName    = "AddLabel"
	RemoveLabelName = "RemoveLabel"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ItemLabelAddedAction   audit.Action = "item.label_added"
	ItemLabelRemovedAction audit.Action = "item.label_removed"
)
View Source
const (
	AddMemberName    = "AddMember"
	RemoveMemberName = "RemoveMember"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ItemMemberAddedAction   audit.Action = "item.member_added"
	ItemMemberRemovedAction audit.Action = "item.member_removed"
)
View Source
const (
	ArchiveContainerName   = "ArchiveContainer"
	UnarchiveContainerName = "UnarchiveContainer"

	// The audit codes. Two, not one: an auditor asking what was taken out of use must not have to
	// read a change list to find out whether the entry means it went or came back (audit.md §2).
	ContainerArchivedAction   audit.Action = "container.archived"
	ContainerUnarchivedAction audit.Action = "container.unarchived"
)
View Source
const (
	ArchiveWorkItemName   = "ArchiveWorkItem"
	UnarchiveWorkItemName = "UnarchiveWorkItem"

	// The audit codes. Two, not one: an auditor asking what was taken out of use must not have to
	// read a change list to find out whether the entry means it went or came back (audit.md §2).
	ItemArchivedAction   audit.Action = "item.archived"
	ItemUnarchivedAction audit.Action = "item.unarchived"
)
View Source
const (
	AssignWorkItemName   = "AssignWorkItem"
	UnassignWorkItemName = "UnassignWorkItem"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ItemAssignedAction   audit.Action = "item.assigned"
	ItemUnassignedAction audit.Action = "item.unassigned"
)
View Source
const (
	AttachMediaName = "AttachMedia"
	DetachMediaName = "DetachMedia"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ItemAttachmentAddedAction   audit.Action = "item.attachment_added"
	ItemAttachmentRemovedAction audit.Action = "item.attachment_removed"
)
View Source
const (
	BulkUpdateWorkItemsName = "BulkUpdateWorkItems"

	// ItemBulkAppliedAction is the audit code for the bulk itself: who sent one, how big it was,
	// and how much of it applied.
	//
	// It does not describe the changes - each operation writes its own entry through the use case
	// that performed it, and the request identifier on all of them is what ties them together
	// (audit.md §2). What this entry adds is the act of sending five hundred changes at once, which
	// no per-entry record shows.
	ItemBulkAppliedAction audit.Action = "item.bulk_applied"
)
View Source
const (
	CreateCalendarFeedName = "CreateCalendarFeed"
	ListCalendarFeedsName  = "ListCalendarFeeds"
	RevokeCalendarFeedName = "RevokeCalendarFeed"

	// The audit codes. A feed is a credential, so both its minting and its revocation are
	// auditable acts in their own right (audit.md §2).
	FeedCreatedAction audit.Action = "calendar.feed_created"
	FeedRevokedAction audit.Action = "calendar.feed_revoked"
	// FeedReadAction is the act the public route performs: somebody fetched a calendar. Declared
	// here so that the fetch and the two management acts share one target type.
	FeedReadAction audit.Action = "calendar.feed_read"
)
View Source
const (
	EditCommentName   = "EditComment"
	DeleteCommentName = "DeleteComment"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	CommentUpdatedAction audit.Action = "comment.updated"
	CommentDeletedAction audit.Action = "comment.deleted"
)
View Source
const (
	UpdateCustomFieldName = "UpdateCustomField"
	DeleteCustomFieldName = "DeleteCustomField"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	CustomFieldUpdatedAction audit.Action = "custom_field.updated"
	CustomFieldDeletedAction audit.Action = "custom_field.deleted"
)
View Source
const (
	UpdateReminderName = "UpdateReminder"
	DeleteReminderName = "DeleteReminder"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ReminderUpdatedAction audit.Action = "reminder.updated"
	ReminderDeletedAction audit.Action = "reminder.deleted"
)
View Source
const (
	UpdateSavedViewName = "UpdateSavedView"
	DeleteSavedViewName = "DeleteSavedView"
	ShareSavedViewName  = "ShareSavedView"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ViewUpdatedAction audit.Action = "view.updated"
	ViewDeletedAction audit.Action = "view.deleted"
	ViewSharedAction  audit.Action = "view.shared"
)
View Source
const (
	UpdateTemplateName = "UpdateTemplate"
	DeleteTemplateName = "DeleteTemplate"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	TemplateUpdatedAction audit.Action = "template.updated"
	TemplateDeletedAction audit.Action = "template.deleted"
)
View Source
const (
	CompleteWorkItemName = "CompleteWorkItem"
	ReopenWorkItemName   = "ReopenWorkItem"

	// ItemCompletedAction and ItemReopenedAction are the audit codes. Stable: an auditor filters on
	// them and a SIEM rule matches on them (audit.md §2).
	ItemCompletedAction audit.Action = "item.completed"
	ItemReopenedAction  audit.Action = "item.reopened"
)
View Source
const (
	CreateBucketName = "CreateBucket"
	ListBucketsName  = "ListBuckets"

	// BucketCreatedAction is the audit code. Stable: an auditor filters on it and a SIEM rule
	// matches on it (audit.md §2).
	BucketCreatedAction audit.Action = "bucket.created"
	// BucketReadAction is the audit code of an attempted read. Declared even though an ordinary read
	// writes no entry: a *refused* read does, and it is recorded against the action that was refused
	// rather than against a generic "denied" (audit.md §4).
	BucketReadAction audit.Action = "bucket.read"
)
View Source
const (
	CreateContainerName = "CreateContainer"

	// ContainerCreatedAction is the audit code. Stable: an auditor filters on it and a SIEM rule
	// matches on it (audit.md §2).
	ContainerCreatedAction audit.Action = "container.created"
)

The identity of this use case, in the one place that decides it. The registry derives the REST operation, the MCP tool and the automation action from the name (usecase.Descriptor).

View Source
const (
	CreateLabelName = "CreateLabel"
	ListLabelsName  = "ListLabels"

	// LabelCreatedAction is the audit code. Stable: an auditor filters on it and a SIEM rule
	// matches on it (audit.md §2).
	LabelCreatedAction audit.Action = "label.created"
	// LabelReadAction is the audit code of an attempted read. Declared even though an ordinary read
	// writes no entry: a *refused* read does, and it is recorded against the action that was
	// refused rather than against a generic "denied" (audit.md §4).
	LabelReadAction audit.Action = "label.read"
)
View Source
const (
	CreateWorkItemName = "CreateWorkItem"

	// ItemCreatedAction is the audit code. Stable: an auditor filters on it and a SIEM rule
	// matches on it (audit.md §2).
	ItemCreatedAction audit.Action = "item.created"
)
View Source
const (
	DefineCustomFieldName = "DefineCustomField"
	ListCustomFieldsName  = "ListCustomFields"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	CustomFieldDefinedAction audit.Action = "custom_field.defined"
	// CustomFieldReadAction is declared even though an ordinary read writes no entry: a *refused*
	// read does, and it is recorded against the action that was refused rather than against a
	// generic "denied" (audit.md §4).
	CustomFieldReadAction audit.Action = "custom_field.read"
)
View Source
const (
	DeleteBucketName = "DeleteBucket"

	// BucketDeletedAction is the audit code. Stable: an auditor filters on it and a SIEM rule
	// matches on it (audit.md §2).
	BucketDeletedAction audit.Action = "bucket.deleted"
)
View Source
const (
	DuplicateWorkItemName = "DuplicateWorkItem"

	// ItemDuplicatedAction is the audit code. One entry per duplication rather than one per entry
	// it produced: the act an auditor asks about is "who copied this, and to where", and the
	// entries the copy consists of are what that act produced rather than acts of their own
	// (audit.md §2).
	ItemDuplicatedAction audit.Action = "item.duplicated"
)
View Source
const (
	ListPrivateHubAccessesName = "ListPrivateHubAccesses"
	ListEmergencyNoticesName   = "ListEmergencyNotices"

	// PrivateHubAccessesReadAction is the code a refused read of a hub's accesses is recorded under.
	PrivateHubAccessesReadAction audit.Action = "container.emergency_accesses_read"

	// EmergencyNoticesReadAction is declared for the catalogue; reading one's own notices refuses
	// nobody anything beyond the credential, so nothing is recorded under it.
	EmergencyNoticesReadAction audit.Action = "container.emergency_notices_read"

	// EmergencyNoticeDays is how long the app shows a member that their hub was opened.
	EmergencyNoticeDays = 30
)
View Source
const (
	ExportViewName = "ExportView"

	// ViewExportedAction is the audit code. An export is a bulk read of somebody's work, which is
	// exactly the act an auditor asks about after the fact (audit.md §2).
	ViewExportedAction audit.Action = "view.exported"
)
View Source
const (
	InstantiateTemplateName = "InstantiateTemplate"

	// TemplateInstantiatedAction is the audit code. One entry for one act, whatever the tree's
	// size (audit.md §2).
	TemplateInstantiatedAction audit.Action = "template.instantiated"
)
View Source
const (
	ListCommentsName = "ListComments"

	// CommentsReadAction is the audit code of an attempted read, declared for the reason
	// ActivityReadAction is: an ordinary read writes no entry, a refused one does.
	CommentsReadAction audit.Action = "comment.list_read"
)
View Source
const (
	ListPrivateHubsName = "ListPrivateHubs"

	// PrivateHubsReadAction is the code a refused read is recorded under.
	PrivateHubsReadAction audit.Action = "private_hubs.read"
)
View Source
const (
	DefaultRuleBatch       = 25
	DefaultOccurrenceBatch = 50
)

Default bounds for one pass.

View Source
const (
	MoveContainerName = "MoveContainer"

	// ContainerMovedAction is the audit code. One code for a change of hub and for a reorder within
	// one, because both are "this collection sits somewhere else now" - an auditor recognises a
	// reorder in the entry by the parent being unchanged (audit.md §2).
	ContainerMovedAction audit.Action = "container.moved"
)
View Source
const (
	MoveWorkItemName    = "MoveWorkItem"
	ReorderWorkItemName = "ReorderWorkItem"

	// ItemMovedAction is the audit code for both operations. One code, because domain-model.md §4 gives
	// movement one event and an auditor asking "who moved this" means both - a reorder is recognisable in the
	// entry by the parent being unchanged (audit.md §2).
	ItemMovedAction audit.Action = "item.moved"
)
View Source
const (
	OpenPrivateHubName = "OpenPrivateHub"

	// EmergencyAccessOpenedAction is the trail's entry for the workspace owner's opening of a
	// private hub (ADR-0073 §4, UC-ID-16 check 4).
	EmergencyAccessOpenedAction audit.Action = "container.emergency_access_opened"
)
View Source
const (
	// DefaultPageSize is what a caller that names no size gets.
	DefaultPageSize = 50
	// MaxPageSize is the ceiling. Bulk export goes through an :export job instead, which is why this
	// is a clamp rather than a refusal: a client asking for 500 rows wants as many as it can have,
	// and 200 of them is a better answer than an error.
	MaxPageSize = 200
)

The page size limits of api-guidelines.md §4, and the shape a paged answer takes.

In the application layer rather than in the REST adapter, because all three channels page: an MCP tool call and an automation action ask for a page the same way a request does, and a limit enforced in one adapter is a limit the other two do not have.

View Source
const (
	ListActivityName = "ListActivity"

	// ActivityReadAction is the audit code of an attempted read, declared for the reason
	// ItemReadAction is: an ordinary read writes no entry, a refused one does. Its own code rather
	// than the entry's, because "who tried to read the history of this" is a question an auditor
	// asks separately from "who tried to read this" (audit.md §4).
	ActivityReadAction audit.Action = "item.activity_read"
)
View Source
const (
	GetContainerName   = "GetContainer"
	ListContainersName = "ListContainers"

	// ContainerReadAction is the audit code of an attempted read. Declared even though an ordinary
	// read writes no entry: a *refused* read does, and it is recorded against the action that was
	// refused rather than against a generic "denied" (audit.md §4, access.Request.Action).
	ContainerReadAction audit.Action = "container.read"
)
View Source
const (
	ListTrashName = "ListTrash"

	// TrashReadAction is the audit code of an attempted read. Declared even though an ordinary read
	// writes no entry: a refused one does, recorded against the action that was refused (audit.md §4).
	TrashReadAction audit.Action = "trash.read"
)
View Source
const (
	GetWorkItemName   = "GetWorkItem"
	ListWorkItemsName = "ListWorkItems"

	// ItemReadAction is the audit code of an attempted read, declared for the same reason
	// ContainerReadAction is: an ordinary read writes no entry, a refused one does.
	ItemReadAction audit.Action = "item.read"
)
View Source
const (
	SetRecurrenceName    = "SetRecurrence"
	RemoveRecurrenceName = "RemoveRecurrence"
	GetRecurrenceName    = "GetRecurrence"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	RecurrenceSetAction     audit.Action = "recurrence.set"
	RecurrenceChangedAction audit.Action = "recurrence.changed"
	RecurrenceRemovedAction audit.Action = "recurrence.removed"
	// RecurrenceReadAction is the audit code of an attempted read, declared for the reason
	// CommentsReadAction is: an ordinary read writes no entry, a refused one does.
	RecurrenceReadAction audit.Action = "recurrence.read"
)
View Source
const (
	CreateReminderName = "CreateReminder"
	ListRemindersName  = "ListReminders"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ReminderCreatedAction audit.Action = "reminder.created"
	// RemindersReadAction is the audit code of an attempted read, declared for the reason
	// CommentsReadAction is: an ordinary read writes no entry, a refused one does.
	RemindersReadAction audit.Action = "reminder.list_read"
)
View Source
const (
	CreateSavedViewName = "CreateSavedView"
	ListSavedViewsName  = "ListSavedViews"
	GetSavedViewName    = "GetSavedView"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ViewCreatedAction audit.Action = "view.created"
	ViewReadAction    audit.Action = "view.read"
)
View Source
const (
	SetCoverName   = "SetCover"
	ClearCoverName = "ClearCover"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ItemCoverSetAction     audit.Action = "item.cover_set"
	ItemCoverClearedAction audit.Action = "item.cover_cleared"
)
View Source
const (
	SetCustomFieldName = "SetCustomField"

	// ItemCustomFieldSetAction is the audit code. Stable: an auditor filters on it and a SIEM rule
	// matches on it (audit.md §2).
	ItemCustomFieldSetAction audit.Action = "item.custom_field_set"
)
View Source
const (
	SetDueDateName   = "SetDueDate"
	ClearDueDateName = "ClearDueDate"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	ItemDueSetAction     audit.Action = "item.due_set"
	ItemDueClearedAction audit.Action = "item.due_cleared"
)
View Source
const (
	SetHubPrivacyName = "SetHubPrivacy"

	// HubPrivacyChangedAction is the audit code: who made a hub private, or shared again. Its own
	// code, because "who shut the administrators out of this hub" is a question an access review
	// asks on its own.
	HubPrivacyChangedAction audit.Action = "container.privacy_changed"
)
View Source
const (
	CreateTemplateName = "CreateTemplate"
	ListTemplatesName  = "ListTemplates"
	GetTemplateName    = "GetTemplate"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	TemplateCreatedAction audit.Action = "template.created"
	// TemplatesReadAction is the audit code of an attempted read, declared for the reason
	// CommentsReadAction is: an ordinary read writes no entry, a refused one does.
	TemplatesReadAction audit.Action = "template.read"
)
View Source
const (
	TrashContainerName   = "TrashContainer"
	RestoreContainerName = "RestoreContainer"

	// The audit codes. `.deleted` rather than `.trashed`, as domain-model.md §4 names the container
	// half of this - the item half is `.trashed` there, and the names are kept as the document
	// writes them rather than harmonised afterwards.
	ContainerDeletedAction  audit.Action = "container.deleted"
	ContainerRestoredAction audit.Action = "container.restored"
)
View Source
const (
	TrashWorkItemName   = "TrashWorkItem"
	RestoreWorkItemName = "RestoreWorkItem"

	// The audit codes. This is the first deletion path in the system, so the two directions are
	// separate codes rather than one with a flag: "what was deleted here" is the question an auditor
	// asks, and answering it must not require reading a change list (audit.md §2).
	ItemTrashedAction  audit.Action = "item.trashed"
	ItemRestoredAction audit.Action = "item.restored"
)
View Source
const (
	UpdateBucketName  = "UpdateBucket"
	ReorderBucketName = "ReorderBucket"

	// The audit codes. Two rather than one, for the reason the container's rename and policy update
	// have two: "who renamed this column" and "who rearranged the board" are different questions of
	// the trail, and an auditor filtering on one must not have to read the change list of the other
	// to find out which it is looking at (audit.md §2).
	BucketUpdatedAction   audit.Action = "bucket.updated"
	BucketReorderedAction audit.Action = "bucket.reordered"
)
View Source
const (
	RenameContainerName         = "RenameContainer"
	UpdateContainerPoliciesName = "UpdateContainerPolicies"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	//
	// Two codes rather than one, because the two answer different questions of the trail. "Who
	// changed how this collection works" is an administrative question and "who renamed it" is not,
	// and an auditor filtering on one must not have to read the change list of the other to find out
	// which it is looking at.
	ContainerRenamedAction         audit.Action = "container.renamed"
	ContainerPoliciesUpdatedAction audit.Action = "container.policies_updated"
)
View Source
const (
	UpdateLabelName = "UpdateLabel"
	DeleteLabelName = "DeleteLabel"

	// The audit codes. Stable: an auditor filters on them and a SIEM rule matches on them
	// (audit.md §2).
	LabelUpdatedAction audit.Action = "label.updated"
	LabelDeletedAction audit.Action = "label.deleted"
)
View Source
const (
	UpdateWorkItemName = "UpdateWorkItem"

	// ItemUpdatedAction is the audit code. Stable: an auditor filters on it and a SIEM rule matches on
	// it (audit.md §2).
	ItemUpdatedAction audit.Action = "item.updated"
)
View Source
const AiTranslateName = "AiTranslate"

AiTranslateName is the catalogue name (core/application/catalogue).

View Source
const AssigneeRestrictedCode = "items.assignee_restricted"

AssigneeRestrictedCode is the answer to a rule that names a person whose processing is restricted: no rule puts them on an entry (UC-PRV-04 check 4, data-protection.md §4). The engine records the action as SKIPPED with it, never as failed.

View Source
const (
	AutoAssignWorkItemName = "AutoAssignWorkItem"
)
View Source
const DefaultReminderBatch = 100

DefaultReminderBatch is how many reminders one pass settles. Large enough that a normal tenant is done in one, small enough that the transaction stays short.

View Source
const EmbeddingSeedName = "search.embedding"

EmbeddingSeedName is how this subscriber is known in the deduplication and in the logs. Stable across versions, for the reason every consumer name is: renaming it makes every event it has already seen look new.

View Source
const QueryItemsName = "QueryItems"

QueryItemsName is the catalogue name (core/application/catalogue).

View Source
const ReasonPrivateHubOrphaned = "private_hub_orphaned"

ReasonPrivateHubOrphaned is the audit entry's reason when the system trashed a private hub nobody was left in.

View Source
const RecurrenceSkippedAction audit.Action = "recurrence.occurrence_skipped"

RecurrenceSkippedAction is the audit code. Stable: an auditor filters on it and a SIEM rule matches on it (audit.md §2).

View Source
const ReindexAskedAction audit.Action = "search.reindex_asked"

ReindexAskedAction records that an administrator asked for the workspace's search documents to be brought current, and how many were stale when they asked.

View Source
const ReindexSearchName = "ReindexSearch"

ReindexSearchName is the catalogue name (core/application/catalogue).

View Source
const ReorderContainerName = "ReorderContainer"

ReorderContainerName is the catalogue name.

View Source
const (
	// SearchItemsName is the catalogue name (core/application/catalogue).
	SearchItemsName = "SearchItems"
)
View Source
const SkipOccurrenceName = "SkipOccurrence"

SkipOccurrenceName is the catalogue's name for the one part of the materialisation a person asks for.

View Source
const TranslationAskedAction audit.Action = "ai.translation_asked"

TranslationAskedAction records that somebody read an entry in another language: the entry and the language, never the text (rule 10). Its own action, because "what was sent to a provider, and about what" is the question a data protection officer asks, and this sends an entry's content somewhere without recording anything the other AI actions record.

Variables

This section is empty.

Functions

func BucketSnapshot

func BucketSnapshot(bucket domain.Bucket) usecase.Output

BucketSnapshot is a column as the board lists it.

func BulkOperationNames

func BulkOperationNames() []string

BulkOperationNames are the operations a bulk accepts, in a stable order. The catalogue declares them as the field's enum, so a client is told what it may send rather than having to try.

func CommentSnapshot

func CommentSnapshot(comment domain.Comment) usecase.Output

CommentSnapshot is a comment as the thread lists it.

func ContainerSnapshot

func ContainerSnapshot(container domain.Container) usecase.Output

ContainerSnapshot is a hub or a collection as `GET /containers/{id}` answers it.

func ItemOutput

func ItemOutput(item domain.WorkItem) usecase.Output

ItemOutput is the shape every channel returns: the field names of the contract

Exported because the lifecycle context answers an entry too: `:retain` hands back the entry it took out of a retention period, and a second mapper for the same shape would be a second place for a field to be forgotten. It is a pure function over the aggregate - no ports, no decisions - which is what makes one service package reading another's a definition rather than a dependency. (api/openapi.yaml, schema WorkItem), so that a REST response, an MCP tool result and an automation action result describe the item in the same words.

func ItemSnapshot

func ItemSnapshot(item domain.WorkItem) usecase.Output

ItemSnapshot is an entry as `GET /items/{id}` answers it.

func LabelSnapshot

func LabelSnapshot(label domain.Label) usecase.Output

LabelSnapshot is a label as the collection lists it.

func PageSize

func PageSize(requested int) int

PageSize clamps a requested size into the contract's range.

func RecurrenceSnapshot

func RecurrenceSnapshot(rule domain.RecurrenceRule) usecase.Output

RecurrenceSnapshot is a series definition as `GET /items/{id}/recurrence` answers it.

func ReminderSnapshot

func ReminderSnapshot(reminder domain.Reminder) usecase.Output

ReminderSnapshot is a reminder as the entry lists it.

func TemplateSnapshot

func TemplateSnapshot(template domain.Template) usecase.Output

TemplateSnapshot is a template as `GET /templates/{id}` answers it.

func TranslationAnswerKeys

func TranslationAnswerKeys() map[string][]string

TranslationAnswerKeys is the allow list the gate reads beside the suggestion service's.

Types

type ActivityJournal

type ActivityJournal struct {
	Entries activityrepo.Journal
	IDs     clock.IDGenerator
}

ActivityJournal writes the steps of an item's own history (domain-model.md §3.5).

func (ActivityJournal) RecordStep

func (j ActivityJournal) RecordStep(
	ctx context.Context, actor appshared.ActorContext, tenantID, itemID, collectionID shared.ID,
	verb activity.Verb, changeSet map[string]any, at time.Time,
) error

RecordStep writes one step of an entry's history on behalf of something that is not a use case: the push, marking a merge or a change that lost. The entry is named by its identifiers rather than handed over whole, because the caller holds the server's copy as the catalogue answered it and not as the domain type.

type AddComment

type AddComment struct {
	Writer CommentWriter
}

AddComment puts a contribution on an entry's discussion.

func (AddComment) Descriptor

func (h AddComment) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (AddComment) Execute

Execute writes the comment and returns it.

func (AddComment) FileDisplaced

func (h AddComment) FileDisplaced(
	ctx context.Context, actor appshared.ActorContext, itemID shared.ID, body string,
	params map[string]string,
) (domain.Comment, error)

FileDisplaced files the version of a free-text field that lost a merge as a system comment on the entry, in the merging person's name (offline-sync.md §5): the body is the text that lost, the heading is `sync.displaced_version` with the field, the device and the reading. The same write as any comment - the same permission, the same event, the same change log entry - so a device that may not comment on the entry is told so, and the push answers the conflict with both values and no comment.

type AddCommentCommand

type AddCommentCommand struct {
	// ID is the identifier the client minted, or empty for one the server mints - a device
	// commenting offline assigns its own (offline-sync.md §3.2), CreateWorkItemCommand's rule.
	ID     shared.ID
	ItemID shared.ID
	// SystemCode and SystemParams make the comment one the server files on the author's behalf
	// (offline-sync.md §5). Not declared as inputs: nobody asks for a system comment over a
	// channel, and FileDisplaced is the one caller.
	SystemCode   string
	SystemParams map[string]string
	// ParentCommentID is the comment being replied to, empty for a top-level comment.
	ParentCommentID shared.ID
	Body            string
}

AddCommentCommand is the input, typed.

type AddLabel

type AddLabel struct {
	Writer ItemLabelWriter
}

AddLabel puts a label on an entry.

func (AddLabel) Descriptor

func (h AddLabel) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (AddLabel) Execute

func (h AddLabel) Execute(
	ctx context.Context, actor appshared.ActorContext, cmd LabelCommand,
) (ItemLabelSet, error)

Execute puts the label on the entry and returns the labels it now carries.

type AddMember

type AddMember struct {
	Writer ItemMemberWriter
}

AddMember puts an account on an entry's member list.

func (AddMember) Descriptor

func (h AddMember) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (AddMember) Execute

Execute puts the account on the entry and returns the members it now carries.

type AiProviders

type AiProviders interface {
	For(ctx context.Context, actor appshared.ActorContext) (aiprovider.Provider, error)
}

AiProviders answers which provider a workspace uses. An interface here rather than the adapter, because the application layer may not import one (ADR-0001).

type AiTranslate

type AiTranslate struct {
	// Reader is the ordinary read of the entry: the permission question and the existence
	// question at once, asked before any provider is called.
	Reader    GetWorkItem
	Providers AiProviders
	Prompts   aiprovider.Prompts
	Audit     audit.Sink
	// UnitOfWork opens the transaction the audit entry is written in. The asking use cases
	// record theirs inside one; this one recorded it outside and met
	// `postgres.no_transaction_in_context` the first time a person asked.
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	// Timeout bounds the provider call. Zero takes the default above.
	Timeout time.Duration
	// Kept is whose content stays out of AI (data-protection.md §4.1). Nil keeps nobody out.
	Kept KeptOutReader
}

AiTranslate reads one entry in another language (ai-first.md §2's translation row).

A read, not a record: the row says "display only, not persisted" and i18n-l10n.md §7 says user content is never translated in place. So there is no AiSuggestion, no acceptance and nothing written - the answer is handed back and gone. It is the second AI call somebody waits for, after the search's meaning, and bounded the same way.

func (AiTranslate) Descriptor

func (h AiTranslate) Descriptor() usecase.Descriptor

Descriptor registers the translation in all three channels.

func (AiTranslate) Execute

Execute asks, or refuses.

The order is the one every AI use case keeps: the entry's own permission first, so that this cannot become a way to learn what a workspace has configured; the consent and the provider after it, so that a workspace with AI switched off is told so whatever the entry is.

type Anchored

type Anchored interface {
	ReachInto(
		ctx context.Context, actor appshared.ActorContext, request access.Request,
		containerID shared.ID,
	) (access.Reach, error)
}

Anchored is the slice of the authorisation service a list anchored to a container needs: not "may I read this", but "how much of it may I read".

Its own interface rather than a method on Authorizer, for the reason that split Visibility and Moderation off: only the two lists over a collection's entries ask it, and every other use case would have to satisfy it in its test for nothing.

type ArchiveContainer

type ArchiveContainer struct {
	Writer ContainerWriter
}

ArchiveContainer makes a hub or a collection read-only, and everything under it with it.

Nothing below is written. A collection inherits an archived hub's state through the read rather than through a stamp of its own (I-C3), so archiving a hub costs one row however many collections sit in it - and unarchiving it restores exactly what it covered, leaving a collection archived in its own right archived.

func (ArchiveContainer) Descriptor

func (h ArchiveContainer) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ArchiveContainer) Execute

Execute archives the container and returns it as it now stands.

type ArchiveContainerCommand

type ArchiveContainerCommand struct {
	ContainerID     shared.ID
	ExpectedVersion int
}

ArchiveContainerCommand is the input, typed. It is the same shape for both verbs: an identifier, and the version the caller read.

type ArchiveWorkItem

type ArchiveWorkItem struct {
	Lifecycle LifecycleWriter
}

ArchiveWorkItem takes an entry out of use without deleting it: kept, and read-only.

Nothing below it is archived with it. An entry's children are entries in their own right, unlike the collections of a hub, so there is no inherited stamp here and no I-C3 to enforce - a work package under an archived task stays writable, and that is the model rather than an oversight.

func (ArchiveWorkItem) Descriptor

func (h ArchiveWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (ArchiveWorkItem) Execute

Execute archives the entry and returns it as it now stands.

type AssignWorkItem

type AssignWorkItem struct {
	Assignment AssignmentWriter
}

AssignWorkItem puts one person on an entry.

Handing an entry from one person to another is this use case rather than an unassignment followed by an assignment: the field is a scalar, so it is one decision, one version and one step of the history - and doing it in two calls would leave a moment in which the work belongs to nobody.

func (AssignWorkItem) Descriptor

func (h AssignWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (AssignWorkItem) Execute

Execute puts the account on the entry and returns it.

type AssignmentCommand

type AssignmentCommand struct {
	ItemID    shared.ID
	AccountID shared.ID
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read none
	// and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

AssignmentCommand is the input of both directions, typed. The account is empty for an unassignment, which is the one field the two do not share.

type AssignmentWriter

type AssignmentWriter struct {
	Items      repository.Items
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Visibility Visibility
	// Accounts answers whether a rule is about to put a restricted person on the entry: no rule
	// does (data-protection.md §4). Nil restricts nobody.
	Accounts   RestrictedAccounts
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

AssignmentWriter is what both directions of assignment need.

One dependency set held by both use cases rather than two copies of thirteen fields, for the reason CompletionWriter is one: the two operations are the same walk in opposite directions - the same guards, the same four records - and the only thing that differs is whether a person arrives or leaves.

type AttachMedia

type AttachMedia struct {
	Writer ItemAttachmentWriter
}

AttachMedia puts a file on an entry.

The bytes are shared, never copied: attaching the same object to three entries is three references to one file, which is what the reference count counts and what the reconciliation job decides deletion by (data-protection.md §5).

func (AttachMedia) Descriptor

func (h AttachMedia) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (AttachMedia) Execute

Execute puts the file on the entry and returns what it now carries.

type AttachmentCommand

type AttachmentCommand struct {
	ItemID  shared.ID
	MediaID shared.ID
}

AttachmentCommand is the input of both directions, typed.

No expected version. An attachment lives beside the entry rather than on its row, so there is no version of the entry that it would be racing - which is also why neither operation spends one. Two devices attaching two different files at once is the case the OR-set exists to serve, and an optimistic lock on the entry would make one of them fail for no reason (offline-sync.md §4.2).

type Authorizer

type Authorizer interface {
	Authorize(ctx context.Context, actor appshared.ActorContext, request access.Request) error
}

Authorizer is the slice of the authorisation service this use case needs. An interface rather than the service, so the use case can be tested without a membership table.

type AutoAssignOutcome

type AutoAssignOutcome struct {
	Assigned bool
	Strategy domain.AutoAssignStrategy
	// Code is the stable reason when nobody was assigned, empty otherwise.
	Code string
}

AutoAssignOutcome is what the run did, beside the entry it did it to. A result rather than an error, because "the policy found nobody" is an answer, not a failure: the create that carries the same machinery must not fail the creation over it, and the two callers should not read two different shapes.

type AutoAssignWorkItem

type AutoAssignWorkItem struct {
	Assignment AssignmentWriter
	Policies   repository.AutoAssignPolicies
	Groups     identityrepo.Groups
	// Accounts answers which candidates are under a restriction of processing. Art. 18 is a
	// technical state, and an automatic decision about somebody is exactly the processing it
	// stops (data-protection.md §4) - so the pool is narrowed here rather than the draw
	// being allowed to land on them.
	Accounts identityrepo.Accounts
	Random   clock.RandomSource
}

AutoAssignWorkItem hands an entry out by the collection's assignment policy instead of a named person (domain-model.md §3.6).

The split of work between the layers is the strategy port's contract: this use case resolves the material - which candidates may actually receive the entry, how loaded they are, where the rotation stands - and the domain's strategy only chooses. Eligibility is the same question the manual assignment asks about its one account, asked about every candidate: somebody who cannot see the entry is skipped, not assigned, and never named in an answer (T-04).

func (AutoAssignWorkItem) Descriptor

func (h AutoAssignWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (AutoAssignWorkItem) Execute

Execute hands the entry out and returns it together with what happened.

type BucketDeletion

type BucketDeletion struct {
	BucketID shared.ID
	// TargetBucketID is the column the entries moved to. Zero when this was the last column on the
	// board, in which case the entries now carry none - the state the collection was in before
	// anybody made a board.
	TargetBucketID shared.ID
	MovedItems     int
}

BucketDeletion is what became of the entries that were in the deleted column.

Reported rather than left to be discovered: a board that silently reassigned a person's cards would be indistinguishable from one that lost them.

type BucketWriter

type BucketWriter struct {
	Buckets    repository.Buckets
	Containers repository.Containers
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Text brings the names people type to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

BucketWriter is what every use case that changes an existing column shares.

One dependency set rather than one per verb: they read the same bucket, ask the same permission question of the same collection, and owe the same four writes - the row, the event, the change log entry and the audit entry. What differs between them is which domain method decides the new state, which is what bucketChange carries.

type BulkOperation

type BulkOperation struct {
	Op     string
	ItemID shared.ID
	// Payload is what the single-entry operation takes in its body, by the field names it takes
	// there. It is not inspected here: the catalogue checks it against the operation's own
	// declaration, so a field the operation does not know comes back named rather than ignored.
	Payload map[string]any
}

BulkOperation is one operation of a bulk: which single-entry operation, on which entry, with the body that operation takes on its own.

type BulkOutcome

type BulkOutcome struct {
	Index  int
	Op     string
	Output usecase.Output
	Err    *shared.Error
}

BulkOutcome is what one operation did. Exactly one of Output and Err is set, except for an operation of an atomic bulk that was rolled back, which carries the rolled-back refusal.

type BulkResult

type BulkResult struct {
	Outcomes []BulkOutcome
	Applied  int
	Failed   int
}

BulkResult is the whole answer: one outcome per operation, in the order they were sent.

type BulkUpdateWorkItems

type BulkUpdateWorkItems struct {
	Catalogue  Catalogue
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
}

BulkUpdateWorkItems applies up to five hundred single-entry operations in one call.

Every operation runs through the catalogue, which means it runs exactly as it would on its own: its own permission check, its own event, its own entry in the item's history, its own per-field records for offline clients and its own metric. One event covering five hundred entries would break a merge in a way nothing downstream recovers from, and one history entry for a bulk would hide what the history exists to show (api-guidelines.md §5).

What this use case owns is therefore only the two things a bulk adds: the order, and the question of what happens when one of them fails.

func (BulkUpdateWorkItems) Descriptor

func (h BulkUpdateWorkItems) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4) - and a bulk is what an agent reaches for when it has worked out that fifty entries need the same change, so the MCP door is the point rather than a side effect (ai-first.md §1.1).

func (BulkUpdateWorkItems) Execute

Execute applies the operations and reports what each one did.

It returns an error only when the bulk itself could not be carried out - an unreadable request, a cap exceeded, a context that ran out. An operation that failed is not that: the bulk was carried out, and what happened to each operation is in the answer. A caller reads the outcomes rather than one status covering five hundred of them.

type BulkUpdateWorkItemsCommand

type BulkUpdateWorkItemsCommand struct {
	// Atomic makes the bulk all or nothing. Without it, the operations that succeeded stay applied.
	Atomic     bool
	Operations []BulkOperation
}

BulkUpdateWorkItemsCommand is the input, typed.

type CalendarFeedWriter

type CalendarFeedWriter struct {
	Feeds      feedrepo.CalendarFeeds
	Views      viewrepo.SavedViews
	Containers repository.Containers
	// Permits answers the view's visibility without writing a denial: a view the caller cannot
	// see is not found, and a DENIED entry per invisible bookmark makes the trail unreadable
	// (T-04, the reasoning GetSavedView already applies).
	Permits    Permitting
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	// Entropy is where the token's secret half comes from. A port, so that production draws from
	// crypto/rand and a test can fix the credential it asserts on (rule 4).
	Entropy clock.Entropy
}

CalendarFeedWriter is what the three feed use cases share.

It lives beside the saved views rather than in a package of its own, because minting a feed is a read of a view: the rule that decides who may see which view is viewVisibleTo, in this package, and a second copy of a visibility rule is how two answers to one security question get into a codebase.

type Catalogue

type Catalogue interface {
	Invoke(
		ctx context.Context, name string, actor appshared.ActorContext, in usecase.Input,
	) (usecase.Output, error)
}

Catalogue is the slice of the use case registry a bulk needs: run one use case by name.

Declared here as an interface rather than taking the registry, for the reason every other narrow port in this package is narrow - and for one more: the registry is built *from* this use case's descriptor, so a direct dependency would be a cycle the composition root cannot wire. What is passed in is bound after the catalogue exists.

type ChangeCommentCommand

type ChangeCommentCommand struct {
	ItemID    shared.ID
	CommentID shared.ID
	Body      string
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

ChangeCommentCommand is the input of both, typed. The body is the edit's; a deletion carries none, which is the one field the two do not share.

type ChangeReminderCommand

type ChangeReminderCommand struct {
	ItemID     shared.ID
	ReminderID shared.ID
	Patch      domain.ReminderPatch
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

ChangeReminderCommand is the input of both, typed. The patch is the update's; a deletion carries none, which is the one field the two do not share.

type ChangeTemplateCommand

type ChangeTemplateCommand struct {
	TemplateID shared.ID
	Patch      domain.TemplatePatch
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

ChangeTemplateCommand is the input of both, typed. The patch is the update's; a deletion carries none, which is the one field the two do not share.

type ClearCover

type ClearCover struct {
	Cover CoverWriter
}

ClearCover takes the cover off an entry.

func (ClearCover) Descriptor

func (h ClearCover) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ClearCover) Execute

Execute takes the cover off the entry and returns it.

type ClearDueDate

type ClearDueDate struct {
	Due DueDateWriter
}

ClearDueDate takes the due date off an entry - the instant, the flag and the zone together, because none of them means anything alone.

func (ClearDueDate) Descriptor

func (h ClearDueDate) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ClearDueDate) Execute

Execute takes the due date off the entry and returns it.

type CommentWriter

type CommentWriter struct {
	Comments   repository.Comments
	Items      repository.Items
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	// Moderation answers the one question the permission cannot: which role the actor holds.
	// Only the edit and the deletion consult it - "only the author or an administrator may
	// change a comment" is their rule, and it is named where it is enforced (Authorization.go).
	Moderation Moderation
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Text brings the body to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
	// Holds are the legal holds a deletion is judged against: under one, the deleted comment keeps
	// its text until the retention pass clears it (data-retention.md §4). Only the deletion reads
	// them.
	Holds lifecyclerepo.LegalHolds
}

CommentWriter is what every use case that writes the discussion shares: the same reads, the same permission question, and the same records - the event outwards, the change log entry for offline clients, and the audit entry. What differs between the verbs is which domain method decides the new state, and - for an edit and a deletion - the author-or-administrator rule.

type CompleteWorkItem

type CompleteWorkItem struct {
	Completion CompletionWriter
}

CompleteWorkItem marks a task, work package or activity done.

Where the collection's policy rolls up, completing the last open child completes the item above it, and so on upwards until something is still open or the top is reached (invariant I-W5).

func (CompleteWorkItem) Descriptor

func (h CompleteWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4) - and completion is the automation trigger and action that matters most, so all three doors leading to this one handler is the point rather than a nicety.

func (CompleteWorkItem) Execute

Execute completes the item and returns it.

type CompletionCommand

type CompletionCommand struct {
	ItemID shared.ID
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read none and
	// accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

CompletionCommand is the input of both directions, typed.

type CompletionWriter

type CompletionWriter struct {
	Items      repository.Items
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	// Jobs is where an ON_COMPLETION series asks for its next occurrence. The completion is what
	// seeds it, which is SY-8's server half: creation is bound to the status transition rather
	// than to the event, so two devices completing the same entry produce one transition and one
	// follow-up (offline-sync.md §8).
	Jobs       queue.Queue
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

CompletionWriter is what both directions of completion need.

One dependency set held by both use cases rather than two copies of eleven fields. The two operations are the same walk in opposite directions - the same guards, the same roll-up, the same three records per item touched - and the only thing that differs is which state is being asked for. Keeping the machinery in one place is what makes "reopening rolls up exactly as completing does" true by construction rather than by two people remembering.

type ContainerLifecycleCommand

type ContainerLifecycleCommand struct {
	ContainerID shared.ID
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read none
	// and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

ContainerLifecycleCommand is the input both verbs take.

type ContainerWriter

type ContainerWriter struct {
	Containers repository.Containers
	// Policies is where the auto_assign key of the policies document is stored: its own row, so
	// the rotation's state can be locked (see work.AutoAssignPolicy). Only UpdateContainerPolicies
	// writes it, for the reason only TrashContainer uses Queue - the dependency set is shared, the
	// verbs are not.
	Policies   repository.AutoAssignPolicies
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Queue is where a deletion asks for the tenant's cleanup to be scheduled. Only TrashContainer
	// uses it; the reasoning is at LifecycleWriter.Queue.
	Queue queue.Queue
	// Text brings the names people type to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

ContainerWriter is what every use case that changes an existing container shares.

One dependency set rather than one per verb: they read the same container, ask the same permission question, and owe the same four writes - the row, the event, the change log entry and the audit entry. What differs between them is which domain method decides the new state.

type CoverCommand

type CoverCommand struct {
	ItemID     shared.ID
	Kind       domain.CoverKind
	ColorToken string
	MediaID    shared.ID
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

CoverCommand is the input of both directions, typed. The cover itself is empty for a clearing, which is the one part the two do not share.

type CoverWriter

type CoverWriter struct {
	Items      repository.Items
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Media      MediaReferences
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

CoverWriter is what both directions of the cover share.

One dependency set held by both use cases rather than two copies, for the reason AssignmentWriter is one: putting a cover on and taking it off are the same walk in opposite directions, and the only thing that differs is whether an image arrives or leaves.

type CreateBucket

type CreateBucket struct {
	Buckets    repository.Buckets
	Containers repository.Containers
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Text brings the names people type to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

CreateBucket adds a column to a collection's board.

Only a collection has a board. A hub holds collections and no items, so a column on one would have nothing to hold - which is the same reasoning that keeps items off a hub, and it is Container.EnsureAcceptsItems that answers it.

func (CreateBucket) Descriptor

func (h CreateBucket) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (CreateBucket) Execute

Execute creates the bucket and returns it.

type CreateBucketCommand

type CreateBucketCommand struct {
	CollectionID shared.ID
	Name         string

	// WipLimit is a number of which zero means "no limit", exactly as the empty string means "not
	// set" for the text fields (domain.BucketAttributes).
	WipLimit     int
	IsDoneBucket bool
	ColorToken   string
	// BeforeBucketID is the column the new one goes to the left of. Empty means the right hand end,
	// which is where a board grows unless somebody says otherwise.
	BeforeBucketID shared.ID
}

CreateBucketCommand is the input, typed.

type CreateCalendarFeed

type CreateCalendarFeed struct{ Writer CalendarFeedWriter }

CreateCalendarFeed mints a feed over one saved view and answers its token, once.

func (CreateCalendarFeed) Descriptor

func (h CreateCalendarFeed) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (CreateCalendarFeed) Execute

func (h CreateCalendarFeed) Execute(
	ctx context.Context, actor appshared.ActorContext, viewID shared.ID,
) (MintedFeed, error)

Execute mints the feed.

The permission is the view's own read. A feed grants exactly what its owner may already read and re-asks that question at every fetch, so minting one is not an escalation and needs no permission beyond seeing the view - which is also why a view the caller cannot see answers "not found" here rather than "forbidden" (T-04).

type CreateContainer

type CreateContainer struct {
	Containers repository.Containers
	Authorizer Authorizer
	// OwnHubs answers who may make a private hub; Grants makes its creator its owner. Both are
	// needed only for a private hub, and a build without them refuses one rather than making a hub
	// nobody holds.
	OwnHubs OwnHubs
	Grants  identityrepository.MembershipGrants
	// PrivateHubs is the workspace's switch, read only for a private hub.
	PrivateHubs PrivateHubsRule
	Events      outbox.Events
	Changes     changelog.ChangeLog
	Audit       audit.Sink
	UnitOfWork  persistence.UnitOfWork
	Clock       clock.Clock
	IDs         clock.IDGenerator
	HLC         clock.HLCSource
	// Text brings the names people type to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

CreateContainer creates a hub or a collection.

It is the reference use case: everything a write in this system owes is visible in one Execute - the permission check before the transaction, the invariants in the domain, and inside one transaction the row, the event, the change log entry for offline clients and the audit entry. A later use case that leaves one of them out is visibly different from this one, which is the point of having a reference at all.

func (CreateContainer) Descriptor

func (h CreateContainer) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry: what the use case is called, what it needs, what it records, and how to run it. Registering it is what makes it reachable through REST, MCP and automation at once (arc42 §4).

func (CreateContainer) Execute

Execute creates the container and returns it.

type CreateContainerCommand

type CreateContainerCommand struct {
	Type     domain.ContainerType
	ParentID shared.ID
	Name     string

	Description string
	Icon        string
	ColorToken  string

	// Private makes a new hub private, its creator its owner (ADR-0073 §2).
	Private bool
}

CreateContainerCommand is the input, typed. The registry hands the use case an untyped map from whichever channel the call arrived through; this is what that map becomes once, here.

type CreateLabel

type CreateLabel struct {
	Labels     repository.Labels
	Containers repository.Containers
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Text brings the names people type to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

CreateLabel adds a label to a collection's vocabulary.

A collection rather than a hub, and rather than the tenant: a label is a vocabulary the people working in one collection agree on, and a workspace-wide list would make every collection pay for every other's (domain-model.md §3.5).

func (CreateLabel) Descriptor

func (h CreateLabel) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (CreateLabel) Execute

Execute creates the label and returns it.

type CreateLabelCommand

type CreateLabelCommand struct {
	CollectionID shared.ID
	Name         string
	ColorToken   string
	Description  string
}

CreateLabelCommand is the input, typed.

type CreateReminder

type CreateReminder struct {
	Writer ReminderWriter
}

CreateReminder puts a reminder on an entry.

func (CreateReminder) Descriptor

func (h CreateReminder) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (CreateReminder) Execute

Execute writes the reminder and returns it.

type CreateReminderCommand

type CreateReminderCommand struct {
	ItemID     shared.ID
	OffsetSpec string
	// Channels as the caller named them, and empty for the default. Strings rather than the
	// domain's type, because an unknown channel is a validation answer rather than a parse error
	// (ADR-0011): the domain names the value it refused.
	Channels   []string
	Recipients []shared.ID
}

CreateReminderCommand is the input, typed.

type CreateSavedView

type CreateSavedView struct {
	Views      viewrepo.SavedViews
	Containers repository.Containers
	Authorizer Authorizer
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	// Text brings the name to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

CreateSavedView saves a view: a query in the DSL, validated here against the same catalogue an ad-hoc one passes, and the layout the client draws it in, stored and never consulted (api-guidelines.md §3).

func (CreateSavedView) Descriptor

func (h CreateSavedView) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (CreateSavedView) Execute

Execute creates the view and returns it.

The permission is the scope's, and it depends on what is asked. A view is a bookmark, so creating one needs READ on its scope's path - anyone who may read a collection may save a query on it. Creating it already shared publishes workspace furniture, which is STRUCTURE, exactly as :share asks. An ACCOUNT-scoped view is self-service, the way changing one's own preferences is: no matrix cell describes saving a private bookmark, and requiring one would mean a viewer could not keep their own (identity.UpdateAccountPreferences is the precedent).

type CreateSavedViewCommand

type CreateSavedViewCommand struct {
	ScopeType view.ViewScope
	ScopeID   shared.ID
	Name      string
	Layout    string
	Query     map[string]any
	Grouping  map[string]any
	// VisibleFields distinguishes nil - not sent, stored empty - from an empty list a client
	// chose, though the two store alike.
	VisibleFields []string
	Sharing       view.Sharing
}

CreateSavedViewCommand is the input, typed.

type CreateTemplate

type CreateTemplate struct {
	Writer TemplateWriter
}

CreateTemplate defines a template.

func (CreateTemplate) Descriptor

func (h CreateTemplate) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (CreateTemplate) Execute

Execute writes the template and returns it.

type CreateTemplateCommand

type CreateTemplateCommand struct {
	Spec domain.TemplateSpec
}

CreateTemplateCommand is the input, typed.

type CreateWorkItem

type CreateWorkItem struct {
	Items      repository.Items
	Quota      ItemQuota
	Buckets    repository.Buckets
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Ownership  Ownership
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// AutoAssign is the machinery behind :auto-assign, reused whole: the create path is
	// its second caller - the policy that applies itself to what a collection creates, and the
	// explicit `auto_assign` ask - and its Assignment writer is also how an explicit assignee is
	// written, with the same guards and the same four records as :assign.
	AutoAssign AutoAssignWorkItem
	// DueDates is the writer the declared due fields dispatch into, reused whole for the same
	// reason.
	DueDates DueDateWriter
	// Labels and Members are the writers the two set fields dispatch into, reused whole: a label
	// put on at creation is the same OR-set element, the same events and the same audit entry as
	// one put on a moment later.
	Labels  ItemLabelWriter
	Members ItemMemberWriter
	// Covers is the writer the declared cover dispatches into, reused whole for the same reason:
	// one place decides what a cover may be, what an image has to be before it may become one, and
	// what the change owes.
	Covers CoverWriter
	// CustomFields is the use case the declared values dispatch into, one key at a time. Whole
	// rather than a slice of it, because what judges a value is the definition in force for the
	// entry's collection, and that resolution is the thing that may not be duplicated.
	CustomFields SetCustomField
	// Text brings the title and the notes to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

func (CreateWorkItem) Descriptor

func (h CreateWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (CreateWorkItem) Execute

Execute creates the item and returns it, together with what automatic assignment did when it ran - nil when it did not, which is a different answer from "it ran and found nobody".

type CreateWorkItemCommand

type CreateWorkItemCommand struct {
	// ID is the identifier the client minted, or empty for one the server mints. A device
	// creating an entry offline assigns its own UUIDv7 so that the entry has its final identity
	// at once and a repeated push is idempotent (offline-sync.md §3.2, §9); through the API a
	// caller may do the same, and one that does is held to the same rule.
	ID   shared.ID
	Type domain.ItemType
	// CollectionID may be left empty when ParentID is given: an item's collection is the one its
	// parent is in, and making a client repeat it is making it possible to contradict.
	CollectionID shared.ID
	ParentID     shared.ID
	Title        string
	Notes        string
	// BucketID is the column of the collection's board the entry starts in, empty for none.
	BucketID shared.ID
	// AssigneeID creates the entry already on somebody: the same checks and the same
	// records as :assign, in the same transaction as the creation. Empty for nobody.
	AssigneeID shared.ID
	// AutoAssign asks the collection's assignment policy to hand the new entry out, explicitly -
	// an enabled policy applies itself without this flag. Contradicts AssigneeID and is refused
	// beside it: a named person and a policy cannot both decide one scalar.
	AutoAssign bool
	// ContentLanguage is the language the entry is written in, and empty takes the creator's
	// locale (i18n-l10n.md §5). What it decides is the text search configuration the entry is
	// indexed under, which is the whole of why it is worth carrying (ADR-0034).
	ContentLanguage string
	// StartAt is when the work begins, nil for no start - the create's own field, a plain
	// attribute beside the notes.
	StartAt *time.Time
	// CalendarUID is the UID a calendar client chose for the entry it is making, and empty for
	// every other creation (offline-sync.md §4.2). The tree answers the entry at that address
	// from then on; nothing else reads it, and nothing ever changes it.
	CalendarUID string
	// Due creates the entry already carrying a due date: the same validation and the same
	// records as PUT /items/{id}/due, in the same transaction as the creation - the way an
	// explicit assignee reuses the :assign machinery. Nil for none.
	Due *domain.DueDate
	// LabelIDs creates the entry already carrying labels of its collection, and MemberIDs already
	// on a member list. Each goes through the writer that owns the set, with the same guards and
	// the same four records as the standalone route, in the same transaction as the creation. Empty
	// for none.
	LabelIDs  []shared.ID
	MemberIDs []shared.ID
	// CustomFields creates the entry already carrying values, each judged against the definition in
	// force for its collection and each written as its own change - the per-key merge rule the
	// standalone route exists to keep. Nil for none.
	CustomFields map[string]any
	// Cover creates the entry already covered: a colour token or an image, with the rules and the
	// records of `PUT /items/{id}/cover`, in the same transaction as the creation. Nil for an entry
	// with no cover. `ItemID` is filled at the dispatch - the caller of a create does not know the
	// identifier yet, which is half of why this field had to be written here rather than left to a
	// second request.
	Cover *CoverCommand
	// BeforeItemID is the sibling the new entry is ranked in front of, and empty puts it at the
	// end. It is the move's own anchor, answered by the same neighbours query and the same
	// refusal - a sibling that is not at this level is `items.before_item_not_in_level` rather
	// than a silent append, because a client that asked for a position and got the end of the list
	// has been ignored.
	BeforeItemID shared.ID
}

CreateWorkItemCommand is the input, typed.

The fields are the ones this use case owns or dispatches - the assignee, the due date, the labels, the members, the cover and the custom fields each into the writer that owns them. A field the catalogue does not declare is refused rather than accepted and dropped: a request carrying one comes back naming it (usecase.field_unknown) instead of returning a 201 for an item that is not what the caller asked for.

type CustomFieldWriter

type CustomFieldWriter struct {
	Fields     repository.CustomFields
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	// Text brings the options and a TEXT value to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

CustomFieldWriter is what an edit and a deletion share.

One dependency set held by both, for the reason the label pair holds one: the two are the same walk - read the definition, resolve the scope it lives in, ask the permission there, write under the optimistic lock - and the only thing that differs is what is written at the end.

type DefineCustomField

type DefineCustomField struct {
	Fields     repository.CustomFields
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	// Text brings the options and a TEXT value to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

DefineCustomField adds a field to a workspace's or a collection's vocabulary.

Two scopes and no more. A field every collection defines separately is a field nobody can filter across; a field only ever defined workspace-wide is one team's vocabulary imposed on every other (domain-model.md §3.5). Which of the two applies is the presence of a collection in the command.

func (DefineCustomField) Descriptor

func (h DefineCustomField) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (DefineCustomField) Execute

Execute defines the field and returns it.

type DefineCustomFieldCommand

type DefineCustomFieldCommand struct {
	// CollectionID is the collection the field belongs to, zero for the whole workspace.
	CollectionID shared.ID
	Key          string
	Kind         domain.CustomFieldKind
	Options      []string
	IsRequired   bool
	// AppliesTo are the item types that carry the field. Empty means the column's default, which
	// is a task alone.
	AppliesTo []domain.ItemType
}

DefineCustomFieldCommand is the input, typed.

type DeleteBucket

type DeleteBucket struct {
	Writer BucketWriter
}

DeleteBucket takes a column off a board and moves its entries somewhere they still exist.

It is not one of the verbs BucketWriter.change covers, and the difference is the point: this one writes two rows' worth of state - the column and every entry that was in it - and returns what became of them. A verb that only stamped the column would leave the entries pointing at something that is no longer on the board, and the foreign key's ON DELETE SET NULL would take a person's board apart silently instead.

A soft delete. The row stays so that a change log entry can still name it and a restore stays possible (offline-sync.md §7); the partial unique index frees the name at the same moment.

func (DeleteBucket) Descriptor

func (h DeleteBucket) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (DeleteBucket) Execute

Execute deletes the column and returns what became of its entries.

type DeleteBucketCommand

type DeleteBucketCommand struct {
	BucketID shared.ID
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read none
	// and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

DeleteBucketCommand is the input, typed.

type DeleteComment

type DeleteComment struct {
	Writer CommentWriter
}

DeleteComment turns a comment into its tombstone.

func (DeleteComment) Descriptor

func (h DeleteComment) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (DeleteComment) Execute

Execute writes the tombstone and returns it.

type DeleteCustomField

type DeleteCustomField struct {
	Writer CustomFieldWriter
}

DeleteCustomField takes a definition out of use.

func (DeleteCustomField) Descriptor

func (h DeleteCustomField) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (DeleteCustomField) Execute

Execute takes the definition out of use.

A soft delete, and what makes it one is the size of the alternative: rewriting `custom_fields` across every entry in a collection would be an unbounded write from one request. The values stay and stop being visible, and a definition recreated under the same key is a new definition that shows none of what the old one held - which the partial unique index makes true rather than promised.

type DeleteCustomFieldCommand

type DeleteCustomFieldCommand struct {
	FieldID         shared.ID
	ExpectedVersion int
}

DeleteCustomFieldCommand is the input, typed.

type DeleteLabel

type DeleteLabel struct {
	Writer LabelWriter
}

DeleteLabel takes a label out of a collection's vocabulary.

A soft delete, and the entries that carried it are not rewritten: the read side filters on the label's own stamp, which is what makes the deletion undoable without having to remember who wore it. That is the difference from a deleted column, whose entries have to go somewhere - a chip that is no longer in the vocabulary simply stops being rendered, while an entry in a column that is no longer on the board would be nowhere at all.

func (DeleteLabel) Descriptor

func (h DeleteLabel) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (DeleteLabel) Execute

Execute deletes the label and returns it as it now stands.

type DeleteLabelCommand

type DeleteLabelCommand struct {
	LabelID         shared.ID
	ExpectedVersion int
}

DeleteLabelCommand is the input, typed.

type DeleteReminder

type DeleteReminder struct {
	Writer ReminderWriter
}

DeleteReminder removes a reminder.

func (DeleteReminder) Descriptor

func (h DeleteReminder) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (DeleteReminder) Execute

Execute removes the reminder.

type DeleteSavedView

type DeleteSavedView struct {
	Writer SavedViewWriter
}

DeleteSavedView removes a view. A calendar feed that served it keeps its token and serves nothing, saying why - the reference nulls rather than cascading (migration 0005).

func (DeleteSavedView) Descriptor

func (h DeleteSavedView) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (DeleteSavedView) Execute

Execute removes the view.

type DeleteTemplate

type DeleteTemplate struct {
	Writer TemplateWriter
}

DeleteTemplate removes a template, leaving the trees it has stamped out standing.

func (DeleteTemplate) Descriptor

func (h DeleteTemplate) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (DeleteTemplate) Execute

Execute removes the template.

type DetachMedia

type DetachMedia struct {
	Writer ItemAttachmentWriter
}

DetachMedia takes a file off an entry. The object itself stays - it may serve other entries, and what decides its life is the count rather than this call.

func (DetachMedia) Descriptor

func (h DetachMedia) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (DetachMedia) Execute

Execute takes the file off the entry and returns what it now carries.

type DueDateCommand

type DueDateCommand struct {
	ItemID shared.ID
	Due    *domain.DueDate
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

DueDateCommand is the input of both directions, typed. The due date is nil for a clearing, which is the one field the two do not share.

type DueDateWriter

type DueDateWriter struct {
	Items      repository.Items
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	// Reminders is here because a due date that moves takes the relative reminders with it: the
	// reminder lives on the same entry, so its moment is recomputed in the same transaction rather
	// than by a job that would leave the two disagreeing until it ran.
	Reminders repository.Reminders
	// Jobs is where the tenant's next wake-up is asked for, in the transaction that moved the
	// date: a reminder that came forward with it needs the schedule to come forward too.
	Jobs       queue.Queue
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

DueDateWriter is what both directions of the due date need.

One dependency set held by both use cases, for the reason AssignmentWriter is one: the two operations are the same walk in opposite directions - the same guards, the same four records - and the only thing that differs is whether a date arrives or leaves. It is also the writer the create and update paths dispatch into: the contract carries the three due fields on their schemas, and serving them through a second implementation would be a second answer to what a due date means.

type DuplicateResult

type DuplicateResult struct {
	Item domain.WorkItem
	// Copied is the size of the copy, the entry itself included.
	Copied int
	// DroppedReferences is what could not be carried over, from every entry that lost something.
	// Empty rather than nil: a client that iterates the losses should not have to nil-check the
	// field.
	DroppedReferences []domain.DroppedReference
}

DuplicateResult is what a duplication answers with: the copy of the entry that was asked about, how many entries the copy consists of, and what the destination could not carry over (I-W6).

type DuplicateWorkItem

type DuplicateWorkItem struct {
	Quota       ItemQuota
	Items       repository.Items
	ItemLabels  repository.ItemLabels
	ItemMembers repository.ItemMembers
	Labels      repository.Labels
	Buckets     repository.Buckets
	Fields      repository.CustomFields
	Containers  repository.Containers
	Attachments mediarepo.Attachments
	Media       MediaReferences
	Profiles    metarepo.CapabilityProfiles
	Authorizer  Authorizer
	// Ownership is the same question the create path asks: does the role the actor holds write only
	// what is assigned to them. A copy is a create, so it has to ask it too.
	Ownership  Ownership
	Visibility Visibility
	// Restrictions answers whether a rule is copying an entry onto a restricted person: the copy is
	// made unassigned and the drop reported (data-protection.md §4). Nil restricts nobody.
	Restrictions RestrictedAccounts
	Events       outbox.Events
	Changes      changelog.ChangeLog
	Audit        audit.Sink
	Activity     ActivityJournal
	UnitOfWork   persistence.UnitOfWork
	Clock        clock.Clock
	IDs          clock.IDGenerator
	HLC          clock.HLCSource
	// Text is what a copied TEXT value is checked with; a value written before the form was
	// applied is composed by the copy, exactly as its next edit would compose it.
	Text text.Normalizer
}

DuplicateWorkItem copies an entry, and everything below it when asked for.

The copy is a new entry rather than a fork of a conversation: what travels is the description - the title, the notes, the column, the labels, the members, the assignee, the cover, the attachments and the custom field values, as far as each type's capability profile allows - and what does not is the discussion and the history. A comment is an exchange between people about one entry, and there is no sense in which a copy has already had it.

What the destination cannot resolve is reported rather than dropped in silence (I-W6), exactly as a move reports it: a label belongs to one collection's vocabulary, a column to one board, a custom field key to whatever definition that collection gives it, and an account may see one collection and not another. A person who spent an afternoon labelling learns what to redo.

func (DuplicateWorkItem) Descriptor

func (h DuplicateWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4) - and copying an entry is an action a template or a rule performs, so the automation door is not a nicety here.

func (DuplicateWorkItem) Execute

Execute copies the entry and returns the copy.

type DuplicateWorkItemCommand

type DuplicateWorkItemCommand struct {
	ItemID shared.ID
	// IncludeSubtree copies everything below the entry with it. Without it the entry is copied
	// alone and its children stay where they are.
	IncludeSubtree bool
	// TargetParentID is the entry the copy sits under, meaningful only together with ParentGiven:
	// the zero value is both "the top level" and "not asked for", and those are different requests.
	TargetParentID shared.ID
	ParentGiven    bool
	// TargetCollectionID may be omitted when a parent is given: an entry's collection is the one
	// its parent is in, and making a client repeat it is making it possible to contradict.
	TargetCollectionID shared.ID
	// Title is the copy's title, and empty keeps the original's. A server that invented "Copy of …"
	// would be writing display text (ADR-0011).
	Title string
}

DuplicateWorkItemCommand is the input, typed.

type EditComment

type EditComment struct {
	Writer CommentWriter
}

EditComment rewrites a comment's text.

func (EditComment) Descriptor

func (h EditComment) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (EditComment) Execute

Execute rewrites the text and returns the comment.

type EmbedItems

type EmbedItems struct {
	Embeddings repository.Embeddings
	Providers  AiProviders
	Semantic   SemanticSearch
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	// BatchSize bounds one pass. Zero takes the default below.
	BatchSize int
	// Kept is whose content stays out of AI: their entries are not embedded, and their vectors
	// are forgotten by the pass (data-protection.md §4.1). Nil keeps nobody out.
	Kept KeptOutReader
}

EmbedItems brings one workspace's vectors up to date with its entries.

Not a use case, and deliberately not in the catalogue: nobody asks for their entries to be embedded. It is the pass behind a job, in the shape the retention sweep has - a batch, then a decision about whether to come straight back - because what it does is a backlog rather than a question.

**Nothing about it is on the write path.** An embedding cannot be maintained by a trigger the way `search_document` is, because a trigger cannot make a network call, and a write that waited on a provider is precisely what ai-first.md §2 forbids. So an entry whose text just changed is found lexically until the pass reaches it, which is the correct degradation rather than a gap: the entry is searchable the whole time, by the words somebody would use to find something they just wrote.

func (EmbedItems) Execute

func (h EmbedItems) Execute(
	ctx context.Context, actor appshared.ActorContext,
) (EmbedOutcome, error)

Execute embeds one batch of what the workspace owes.

Three refusals, and all three are "nothing to do" rather than failures: no store, no provider that can embed, and nothing owed. Semantic search is optional at every level - the extension, the provider, the consent - and a pass that treated any of them as an error would fill a dead letter queue with a feature somebody has switched off.

type EmbedOutcome

type EmbedOutcome struct {
	// Embedded is how many vectors were written.
	Embedded int
	// Exhausted is true when the pass filled its batch, which means there is probably more owed.
	Exhausted bool
}

EmbedOutcome is what one pass did, for the job that decides whether to come back.

type EmbeddingQueue

type EmbeddingQueue interface {
	Enqueue(ctx context.Context, request queue.Request) (shared.ID, error)
}

EmbeddingQueue is the slice of the job queue this subscriber needs. Narrow, so what it can do is one line: ask for work.

type EmergencyNotices

type EmergencyNotices interface {
	EmergencyAccessOpened(ctx context.Context, access domain.EmergencyAccess, members []shared.ID) error
}

EmergencyNotices tells a private hub's members that it was opened - the notification service's RecordEmergencyAccess - inside the caller's transaction.

type ExportView

type ExportView struct {
	Views      viewrepo.SavedViews
	Containers repository.Containers
	Permits    Permitting
	// Query is the same use case an ad-hoc query goes through. Held rather than reimplemented:
	// an export that selected rows its own way would be a second answer to "what does this view
	// contain", and the two would drift.
	Query      QueryItems
	ItemLabels repository.ItemLabels
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
}

ExportView renders a saved view's result whole.

The rows are selected here and rendered by whichever channel asked: a wire format is an adapter's business (project-structure.md §3), and the backend composes no document text of its own (rule 8). What this use case owes is the selection, the cap, and the honest statement that the cap was reached.

The query executes under the **caller's** authorisation, exactly as reading the view does: two people exporting one shared view export two different files, and neither gets a row their role hides (T-04, T-05).

func (ExportView) Descriptor

func (h ExportView) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ExportView) Execute

func (h ExportView) Execute(
	ctx context.Context, actor appshared.ActorContext, viewID shared.ID,
) (ExportedView, error)

Execute selects the rows.

It walks the query's own pages rather than asking for one large one: the page size the whole product shares is 200, the statements are written and indexed for that, and an export is the one caller that wants all of them - so it asks repeatedly and stops at the cap. One page beyond the cap is what tells it there was more, which is why the walk stops on the first page that crosses it rather than on an exact count nobody asked the database for.

func (ExportView) Select

func (h ExportView) Select(
	ctx context.Context, actor appshared.ActorContext, viewID shared.ID,
) (ExportedView, error)

Select is the selection without the audit entry, for a caller that polls: the CalDAV calendar fetches a view every quarter of an hour as its owner, exactly as the ICS feed does, and an audit row per poll would bury the entries somebody actually looks for (audit.md §2). The same visibility rule, the same walk, the same cap - only the record differs, which is why this is a method on the export rather than a second selection.

type ExportedView

type ExportedView struct {
	View  view.SavedView
	Items []domain.WorkItem
	// Labels is the label identifiers per entry, so that a rendering that shows them does not
	// have to ask again.
	Labels map[shared.ID][]shared.ID
	// Truncated says the result reached view.MaxExportRows and there was more behind it.
	Truncated bool
	// TimeZone is the zone the selection ran under - the caller's own, or the feed owner's. It
	// decides nothing about which rows came back and everything about which day an all-day entry
	// falls on when one is rendered (i18n-l10n.md §4).
	TimeZone    string
	GeneratedAt time.Time
}

ExportedView is what an export answers.

type FireReminders

type FireReminders struct {
	Reminders repository.Reminders
	Items     repository.Items
	// Schedule is the scan the two announcements run on: the same repository as Items, under the
	// narrow interface that says what this pass may do with it.
	Schedule    repository.DueAnnouncements
	Containers  repository.Containers
	ItemMembers repository.ItemMembers
	// Visibility answers whether a named recipient can still see the entry. A membership revoked
	// since the reminder was written is not survived by a reminder that remembers better days -
	// the same question the write asked, asked again at the moment it matters.
	Visibility Visibility
	Notifier   ReminderNotifier
	// Events is where the two scheduling announcements go: item.due_soon and item.overdue are
	// facts about a deadline rather than messages to a person, and what reacts to them is
	// automation (domain-model.md §4).
	Events outbox.Events
	// Changes is how a device learns that a reminder fired. offline-sync.md §8 requires a client
	// to reconcile the local notification it scheduled, and it can only do that if the server's
	// decision reaches it - so the state the pass writes travels like any other field.
	Changes changelog.ChangeLog
	Clock   clock.Clock
	IDs     clock.IDGenerator
	HLC     clock.HLCSource
	Signals ReminderSignals
	// BatchSize bounds one pass. A pass is one transaction, and a transaction that fired ten
	// thousand reminders would hold its locks for as long as that took.
	BatchSize int
}

FireReminders turns the reminders whose moment has come into notifications.

Internal, and deliberately absent from the catalogue in domain-model.md §5, for the reason ReconcileMedia is absent from it: the catalogue is what a person, an agent or a rule can ask for, and "fire everybody's reminders now" is not something anybody should be able to ask for - the way to influence when a reminder fires is the reminder.

It runs inside the transaction the job runner opened, which is the whole reliability argument: the guarded transition, the notification records, the delivery jobs and the job's own completion commit together. A process killed halfway leaves none of them and the reminders are still pending when the job is claimed again - nothing is lost and nothing is doubled (test RT-3).

func (FireReminders) Execute

Execute fires what is due for the tenant the actor names.

type GetContainer

type GetContainer struct {
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

GetContainer reads one hub or collection.

Read-only throughout, which is not a detail: the transaction may be served by a read replica (multi-tenancy.md §7), and a read that opened a write transaction would pin every list in the product to the primary.

func (GetContainer) Descriptor

func (h GetContainer) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (GetContainer) Execute

Execute returns the container.

type GetContainerQuery

type GetContainerQuery struct {
	ContainerID shared.ID
}

GetContainerQuery is the input, typed.

type GetRecurrence

type GetRecurrence struct {
	Recurrences repository.Recurrences
	Items       repository.Items
	Containers  repository.Containers
	Authorizer  Authorizer
	UnitOfWork  persistence.UnitOfWork
}

GetRecurrence reads an entry's series.

The right to read the entry is the right to read what it repeats by, for the reason the history draws the same line: a separate right would be one more thing to get wrong for no protection gained (domain-model.md §3.2).

func (GetRecurrence) Descriptor

func (h GetRecurrence) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (GetRecurrence) Execute

func (h GetRecurrence) Execute(
	ctx context.Context, actor appshared.ActorContext, itemID shared.ID,
) (domain.RecurrenceRule, error)

Execute returns the entry's series, or reports that it has none.

type GetSavedView

type GetSavedView struct {
	Views      viewrepo.SavedViews
	Containers repository.Containers
	Permits    Permitting
	UnitOfWork persistence.UnitOfWork
}

GetSavedView answers one view.

func (GetSavedView) Descriptor

func (h GetSavedView) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (GetSavedView) Execute

func (h GetSavedView) Execute(
	ctx context.Context, actor appshared.ActorContext, id shared.ID,
) (view.SavedView, error)

Execute returns the view, or that there is none.

A view the caller may not see answers exactly what a missing one answers (T-04): another account's private bookmark, and a shared view over a scope the caller holds nothing on, are both worlds this caller cannot distinguish from empty ones.

type GetTemplate

type GetTemplate struct {
	Writer TemplateWriter
}

GetTemplate reads one template.

func (GetTemplate) Descriptor

func (h GetTemplate) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (GetTemplate) Execute

func (h GetTemplate) Execute(
	ctx context.Context, actor appshared.ActorContext, id shared.ID,
) (domain.Template, error)

Execute returns the template, if the caller may see where it is defined.

type GetWorkItem

type GetWorkItem struct {
	Items      repository.Items
	ItemLabels repository.ItemLabels
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

GetWorkItem reads one task, work package or activity.

func (GetWorkItem) Descriptor

func (h GetWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (GetWorkItem) Execute

Execute returns the item.

Two reads before the permission question, not one: the item names its collection, and the collection names the hub. Both are needed because a membership at either applies downwards, and a path that stopped at the collection would refuse somebody who holds the right at the hub (domain-model.md §3.2). They are one round trip apart and inside the same read-only transaction.

type GetWorkItemQuery

type GetWorkItemQuery struct {
	ItemID shared.ID
	// CalendarUID names the entry by the address a calendar client knows it by instead of by its
	// identifier (offline-sync.md §4.2) - the tree's question when a PUT arrives at an address the
	// view does not answer. Read when ItemID is empty; the permission is decided the same way,
	// because the entry found is the same entry.
	CalendarUID string
	// ExpandLabels asks for the labels the entry carries.
	//
	// Asked for rather than always included, which is what `expand` is for (api-guidelines.md §4):
	// the labels are a second query, and a client rendering a plain list should not pay for chips
	// it is not drawing. Absent rather than empty when it was not asked for - so that a client can
	// tell "this entry has no labels" from "I did not ask".
	ExpandLabels bool
}

GetWorkItemQuery is the input, typed.

type InstantiateTemplate

type InstantiateTemplate struct {
	Quota       ItemQuota
	Writer      TemplateWriter
	Items       repository.Items
	ItemMembers repository.ItemMembers
	// Visibility answers whether a node's fixed assignee can see the collection the tree lands in.
	// The same question an assignment asks, asked here because a template written last year
	// may name somebody who has left.
	Visibility Visibility
	// Restrictions answers whether a rule is instantiating for a node's restricted assignee: the
	// entry is made unassigned and the drop reported (data-protection.md §4). Nil restricts nobody.
	Restrictions RestrictedAccounts
	Events       outbox.Events
	Activity     ActivityJournal
}

InstantiateTemplate stamps a template out into a collection.

A sibling of DuplicateWorkItem rather than the same machinery: there is no source tree to read - the shape is a document - but everything else is the same shape of problem, and the answers are the ones the copy already gave. Every entry carries its own records, because a client synchronising the collection has to learn about every row; the references the destination cannot carry are reported rather than dropped silently (I-W6); and the whole act is one audit entry and one announcement.

func (InstantiateTemplate) Descriptor

func (h InstantiateTemplate) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (InstantiateTemplate) Execute

Execute stamps the template out and returns the tree's root.

type InstantiateTemplateCommand

type InstantiateTemplateCommand struct {
	TemplateID   shared.ID
	CollectionID shared.ID
	ParentID     shared.ID
	// Anchor is the day the relative dates count from, and the zero time for "now". A day rather
	// than an instant: "+3 days for a project starting Monday" is about days, and it is read in
	// the caller's own zone.
	Anchor time.Time
	Title  string
}

InstantiateTemplateCommand is the input, typed.

type InstantiationResult

type InstantiationResult struct {
	TemplateID shared.ID
	Root       domain.WorkItem
	// Created is the size of the tree, the root included.
	Created int
	// DroppedReferences is what the destination could not carry, from every entry that lost
	// something. Empty rather than nil: a client that iterates the losses should not have to
	// nil-check the field.
	DroppedReferences []domain.DroppedReference
}

InstantiationResult is what an instantiation answers with.

type ItemAttachmentSet

type ItemAttachmentSet struct {
	ItemID   shared.ID
	MediaIDs []shared.ID
}

ItemAttachmentSet is what an entry carries, as every channel reports it.

type ItemAttachmentWriter

type ItemAttachmentWriter struct {
	Items       repository.Items
	Containers  repository.Containers
	Profiles    metarepo.CapabilityProfiles
	Attachments mediarepo.Attachments
	Media       MediaReferences
	Authorizer  Authorizer
	Events      outbox.Events
	Changes     changelog.ChangeLog
	Audit       audit.Sink
	Activity    ActivityJournal
	UnitOfWork  persistence.UnitOfWork
	Clock       clock.Clock
	IDs         clock.IDGenerator
	HLC         clock.HLCSource
}

ItemAttachmentWriter is what both directions share.

The tokens are the entry's rather than the media context's, for the reason the label pair's are the entry's: attaching a file to an entry is work on that entry, and somebody who may edit it may attach to it. Staging the file needed `media:write`; putting it on an entry needs the right to change the entry (security.md §5, domain-model.md §3.2).

type ItemLabelSet

type ItemLabelSet struct {
	ItemID   shared.ID
	LabelIDs []shared.ID
}

ItemLabelSet is the labels an entry carries, as every channel reports them.

type ItemLabelWriter

type ItemLabelWriter struct {
	Items      repository.Items
	ItemLabels repository.ItemLabels
	Labels     repository.Labels
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

ItemLabelWriter is what both use cases share.

The tokens are the item's rather than the container's, unlike the label itself: defining a collection's vocabulary is structure, and tagging an entry with a word from it is work. Somebody who may edit entries may label them, and somebody who may not must not be able to reach around through this endpoint (security.md §5, domain-model.md §3.2).

type ItemMemberSet

type ItemMemberSet struct {
	ItemID     shared.ID
	AccountIDs []shared.ID
}

ItemMemberSet is the members an entry carries, as every channel reports them.

type ItemMemberWriter

type ItemMemberWriter struct {
	Items       repository.Items
	ItemMembers repository.ItemMembers
	Containers  repository.Containers
	Profiles    metarepo.CapabilityProfiles
	Authorizer  Authorizer
	Visibility  Visibility
	// Accounts answers whether a rule is about to put a restricted person on the entry: no rule
	// does (data-protection.md §4). Nil restricts nobody.
	Accounts   RestrictedAccounts
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

ItemMemberWriter is what both use cases share.

The same shape ItemLabelWriter has, because the member list is the same kind of thing as the label set: an OR-set beside the entry rather than a field on it. What differs is the question asked before the write - a label has to belong to the entry's collection, an account has to be able to see the entry - and the capability that gates it.

type ItemQuota

type ItemQuota interface {
	Items(ctx context.Context, tenant string, adding int64) error
}

CreateWorkItem creates a task, a work package, or an activity.

It is the second walking skeleton, and it owes what CreateContainer owes: the permission check before the transaction, the invariants in the domain, and inside one transaction the row, the event, the change log entry for offline clients and the audit entry.

What is new here is that the rules are data. Which type may sit under which, and how deep, come from the capability profiles through the hierarchy service (ADR-0006) - so this file, like that one, names none of the three levels. ItemQuota is the §4 items ceiling, asked before an entry is created. The slice the work package needs of quota.Guard.

type Jobs

type Jobs interface {
	Enqueue(ctx context.Context, request queue.Request) (shared.ID, error)
}

Jobs is the slice of the queue this use case needs.

type KeptOutReader

type KeptOutReader interface {
	Read(ctx context.Context, tenantID shared.ID) (integration.KeptOut, error)
}

KeptOutReader answers whose content stays out of AI, inside the caller's transaction. Declared here because the application layer may not import a sibling service.

type LabelCommand

type LabelCommand struct {
	ItemID  shared.ID
	LabelID shared.ID
}

LabelCommand is the input of both directions, typed.

No expected version. A label lives beside the entry rather than on its row, so there is no version of the entry that an addition would be racing - which is also why neither operation spends one. Two devices adding two different labels at once is the case the OR-set exists to serve, and an optimistic lock on the entry would make one of them fail for no reason (offline-sync.md §4.2).

type LabelWriter

type LabelWriter struct {
	Labels     repository.Labels
	Containers repository.Containers
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Text brings the names people type to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

LabelWriter is what both use cases that change an existing label share.

One dependency set rather than two: they read the same label, ask the same permission question of the same collection, and owe the same four writes. What differs is which domain method decides the new state and what the change log calls it.

type LastMember

type LastMember struct {
	Hubs   repository.OrphanedHubs
	Writer ContainerWriter
	// Owners tells the workspace's owners. Optional: wired without it the hub still goes to the
	// trash, which is the half nobody could do by hand.
	Owners OwnersNotice
}

LastMember moves a private hub into the trash once nobody is left in it (ADR-0073 §5).

Nobody else may open a private hub, so one without a member is a hub nobody can reach - not the workspace's owners, who hold nothing in it. Trashed rather than purged: the ordinary grace runs, and the workspace's owners are told that one went, without its name. Taking it over is the emergency access; letting it go is the purge.

Asked by whatever ends a membership - a revocation, an erasure - in that act's own transaction, and by the retention pass for whatever a path missed. A member is an account, neither anonymised nor deleted, holding a role on the hub, on a collection in it or on an entry in one.

func (LastMember) AfterMemberLeft

func (l LastMember) AfterMemberLeft(
	ctx context.Context, tenantID shared.ID, named []shared.ID,
) (int, error)

AfterMemberLeft trashes those of the hubs the named hubs, collections or entries sit under that have no member left, inside the caller's transaction, and answers how many went. Nothing named asks about nothing.

func (LastMember) HeldBy

func (l LastMember) HeldBy(ctx context.Context, accountID shared.ID) ([]shared.ID, error)

HeldBy answers the private hubs the account is a member of, for a caller about to remove the account's memberships wholesale.

func (LastMember) Sweep

func (l LastMember) Sweep(ctx context.Context, tenantID shared.ID) (int, error)

Sweep trashes every private hub of the workspace without a member: the retention pass's catch-all for a hub a path missed - a cascade, a pod older than this rule.

type LifecycleCommand

type LifecycleCommand struct {
	ItemID shared.ID
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read none
	// and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

LifecycleCommand is the input every lifecycle verb takes: which entry, and the version the caller read. The same shape for all of them, because they all take exactly that.

type LifecycleWriter

type LifecycleWriter struct {
	Items      repository.Items
	Containers repository.Containers
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Queue is where a deletion asks for the tenant's cleanup to be scheduled.
	//
	// A deletion scheduling its own cleanup is not only convenient, it is the only shape available:
	// nothing in this system may enumerate tenants - the `tenant` table is behind row level security
	// with no bypass for the application role - so no scheduler can create one sweep job per tenant.
	// The job reschedules itself forever afterwards (queue.KindRetentionSweep); provisioning and
	// every session opened seed it too, so a deletion is not the only way a workspace gets one.
	Queue queue.Queue
}

LifecycleWriter is what every verb that moves an entry between the archive and the trash needs.

One dependency set held by all of them rather than a copy per verb. They are the same walk with a different transition in the middle - read the entry and the collection it is in, ask whether the actor may, apply the transition, write it, and record the three things a change owes - and keeping the machinery in one place is what makes "restoring records what trashing records" true by construction rather than by somebody remembering.

type ListActivity

type ListActivity struct {
	History    activityrepo.History
	Items      repository.Items
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListActivity reads one entry's history, newest first.

The right to read the entry is the right to read what happened to it. There is no narrower permission and there should not be: the history says who moved a thing somebody can already see, and a separate right would be one more thing to get wrong for no protection gained (domain-model.md §3.2).

Read-only throughout, which is not a detail: the transaction may be served by a read replica (multi-tenancy.md §7), and a read that opened a write transaction would pin it to the primary.

func (ListActivity) Descriptor

func (h ListActivity) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListActivity) Execute

Execute returns one page of the entry's history.

Two transactions, and the permission question between them. It has to be between them: a refusal writes an audit entry, and an entry written inside a read-only transaction cannot be written at all - which would turn "you may not read this" into "the database is unavailable" (audit.md §7).

type ListActivityQuery

type ListActivityQuery struct {
	ItemID shared.ID
	Cursor string
	Size   int
}

ListActivityQuery is the input, typed.

type ListBuckets

type ListBuckets struct {
	Buckets    repository.Buckets
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListBuckets reads a collection's board.

Read-only throughout, which is not a detail: the transaction may be served by a read replica (multi-tenancy.md §7), and a read that opened a write transaction would pin every board in the product to the primary.

func (ListBuckets) Descriptor

func (h ListBuckets) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListBuckets) Execute

func (h ListBuckets) Execute(
	ctx context.Context, actor appshared.ActorContext, query ListBucketsQuery,
) ([]domain.Bucket, error)

Execute returns the collection's board, left to right.

Not paged, unlike the container and item lists: a board has as many columns as fit on a screen, and the contract returns a plain array (api-guidelines.md §2). A collection that grows thousands of them has a different problem than pagination solves.

type ListBucketsQuery

type ListBucketsQuery struct {
	CollectionID shared.ID
}

ListBucketsQuery is the input, typed.

type ListCalendarFeeds

type ListCalendarFeeds struct{ Writer CalendarFeedWriter }

ListCalendarFeeds answers the caller's own feeds.

func (ListCalendarFeeds) Descriptor

func (h ListCalendarFeeds) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListCalendarFeeds) Execute

Execute reads them. Never anybody else's, whatever the role: a feed is a credential its owner holds, and an administrator who could list one could subscribe to it.

type ListComments

type ListComments struct {
	Comments   repository.Comments
	Items      repository.Items
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListComments reads one entry's discussion, oldest first - a conversation reads top down.

The right to read the entry is the right to read what was said about it, for the reason the history draws the same line: a separate right would be one more thing to get wrong for no protection gained (domain-model.md §3.2). Tombstones are in the page - that is what keeps a thread with deletions readable - and they travel without their body, which is already gone from the row rather than hidden by this reader.

func (ListComments) Descriptor

func (h ListComments) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListComments) Execute

Execute returns one page of the entry's discussion.

Two transactions, and the permission question between them, for the reason ListActivity splits there: a refusal writes an audit entry, and an entry written inside a read-only transaction cannot be written at all (audit.md §7).

type ListCommentsQuery

type ListCommentsQuery struct {
	ItemID shared.ID
	Cursor string
	Size   int
}

ListCommentsQuery is the input, typed.

type ListContainers

type ListContainers struct {
	Containers repository.Containers
	Authorizer Authorizer
	Reader     Reader
	UnitOfWork persistence.UnitOfWork
}

ListContainers reads one level of the container tree.

func (ListContainers) Descriptor

func (h ListContainers) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListContainers) Execute

Execute returns one page of the level.

The permission check has two shapes, because the two levels are different questions.

A level under a named hub is a question about that hub: the client named it, so a refusal is a refusal, and one check covers every row in it - a membership at the hub applies to all its collections. Answering an empty page instead would tell a client that has no access the same thing as one whose hub is empty.

The hub level is anchored to nothing, so there is no single path to check. Checking the tenant scope would refuse everybody whose membership sits on a hub rather than on the workspace, which is most of what hub-scoped memberships are for. So the page is read and then narrowed to the hubs the actor may see: "what can I see" is answered with what they can see, not with a 403.

type ListContainersQuery

type ListContainersQuery struct {
	// ParentID is the hub whose collections are wanted. Empty means the hubs, which is the one level
	// anchored to nothing.
	ParentID shared.ID
	// Type narrows the level; empty means both kinds.
	Type            domain.ContainerType
	IncludeArchived bool
	Cursor          string
	Size            int
}

ListContainersQuery is the input, typed.

type ListCustomFields

type ListCustomFields struct {
	Fields     repository.CustomFields
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListCustomFields reads the definitions in force.

func (ListCustomFields) Descriptor

func (h ListCustomFields) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListCustomFields) Execute

Execute returns the definitions.

Unpaged, deliberately. A workspace's vocabulary is small and bounded by what a person can fill in on one form; a client renders the whole of it or none of it, and a cursor over a list nobody scrolls would be machinery for its own sake.

type ListCustomFieldsQuery

type ListCustomFieldsQuery struct {
	// CollectionID is the collection whose definitions are wanted, together with the
	// workspace-wide ones above it. Zero answers the workspace-wide ones alone.
	CollectionID shared.ID
}

ListCustomFieldsQuery is the input, typed.

type ListEmergencyNotices

type ListEmergencyNotices struct {
	Accesses   repository.EmergencyAccesses
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
}

ListEmergencyNotices answers the person's own notices of the last 30 days: each time the workspace's owner opened a private hub they are a member of, from the record written when it was opened, whatever the mail about it did. It is how a person without a mailbox - a child's managed account - or one whose mail failed is told (ADR-0073 §4, identity.md §22). Not a list of notifications: one kind of notice, and nothing to mark read.

func (ListEmergencyNotices) Descriptor

func (h ListEmergencyNotices) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListEmergencyNotices) Execute

Execute lists them.

type ListLabels

type ListLabels struct {
	Labels     repository.Labels
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListLabels reads a collection's vocabulary.

Read-only throughout: the transaction may be served by a read replica (multi-tenancy.md §7).

func (ListLabels) Descriptor

func (h ListLabels) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListLabels) Execute

func (h ListLabels) Execute(
	ctx context.Context, actor appshared.ActorContext, query ListLabelsQuery,
) ([]domain.Label, error)

Execute returns the collection's labels, by name.

Not paged, for the reason a board is not: a vocabulary people have to agree on is as long as a person can hold in their head, and the contract returns a plain array (api-guidelines.md §2).

type ListLabelsQuery

type ListLabelsQuery struct {
	CollectionID shared.ID
}

ListLabelsQuery is the input, typed.

type ListPrivateHubAccesses

type ListPrivateHubAccesses struct {
	Accesses   repository.EmergencyAccesses
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListPrivateHubAccesses answers every emergency access to a hub, newest first - the members' record of who opened their hub, when and why (ADR-0073 §4). Whoever reads the hub reads it: its members, and the owner while their access runs; for everybody else the hub is not there.

func (ListPrivateHubAccesses) Descriptor

func (h ListPrivateHubAccesses) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListPrivateHubAccesses) Execute

Execute lists them.

type ListPrivateHubs

type ListPrivateHubs struct {
	Hubs       repository.PrivateHubs
	Policies   lifecyclerepo.Policies
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
	// Accesses, Owners and Clock answer each row's emergency access for the caller (ADR-0073
	// §4): whether they may open the hub now, and until when theirs runs. Without them every row
	// says it is not offered - the safe answer for a control that must not be drawn where the
	// server would refuse it (P-05).
	Accesses repository.EmergencyAccesses
	Owners   Permitting
	Clock    clock.Clock
}

ListPrivateHubs answers the administrators what ADR-0073 §3 lets them know of the workspace's private hubs: that they exist, whose they are, how much they hold, and when a trashed one goes - no name, no description, no content.

MANAGE_MEMBERS rather than READ_CONFIGURATION: the list is for running the workspace - a person leaving, the storage they leave behind - which is membership management, and an auditor reads how the workspace is configured, not the people's hubs.

func (ListPrivateHubs) Descriptor

func (h ListPrivateHubs) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListPrivateHubs) Execute

Execute lists them.

type ListReminders

type ListReminders struct {
	Reminders  repository.Reminders
	Items      repository.Items
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListReminders reads one entry's reminders, oldest first.

The right to read the entry is the right to read what it reminds about, for the reason the discussion draws the same line: a separate right would be one more thing to get wrong for no protection gained (domain-model.md §3.2). Unpaged, because what one entry may carry is bounded where reminders are written.

func (ListReminders) Descriptor

func (h ListReminders) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListReminders) Execute

Execute returns the entry's reminders.

Two transactions, and the permission question between them, for the reason ListComments splits there: a refusal writes an audit entry, and an entry written inside a read-only transaction cannot be written at all (audit.md §7).

type ListRemindersQuery

type ListRemindersQuery struct {
	ItemID shared.ID
}

ListRemindersQuery is the input, typed.

type ListSavedViews

type ListSavedViews struct {
	Views      viewrepo.SavedViews
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListSavedViews answers the views the caller may see.

func (ListSavedViews) Descriptor

func (h ListSavedViews) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListSavedViews) Execute

Execute returns the views.

Unpaged, deliberately, the way the custom field list is: a shelf of bookmarks is small and bounded by what a person saves, and a cursor over a list nobody scrolls would be machinery for its own sake. What is shared along the path is computed in the query, never filtered after a page (domain-model.md §3.2).

type ListSavedViewsQuery

type ListSavedViewsQuery struct {
	// ContainerID widens the answer to what is shared along that container's path. Zero answers
	// the caller's own views alone.
	ContainerID shared.ID
}

ListSavedViewsQuery is the input, typed.

type ListTemplates

type ListTemplates struct {
	Templates  repository.Templates
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
}

ListTemplates reads the templates reachable from a container's path.

func (ListTemplates) Descriptor

func (h ListTemplates) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListTemplates) Execute

Execute returns one page of what the caller may use.

type ListTemplatesQuery

type ListTemplatesQuery struct {
	ContainerID shared.ID
	Cursor      string
	Size        int
}

ListTemplatesQuery is the input, typed.

type ListTrash

type ListTrash struct {
	Trash      repository.Trash
	Reader     Reader
	UnitOfWork persistence.UnitOfWork
	// Policies and TombstoneWindow are the two numbers the retention pass removes by - the trash
	// period it reads and the installation's offline window the Purger is given - so that the date
	// each entry carries is the pass's own.
	Policies        lifecyclerepo.Policies
	TombstoneWindow time.Duration
	// Holds and Contributors are what says whether a legal hold keeps a deletion: its rows are
	// judged as the retention pass judges them (lifecycle.Holds.Blocking), so `held` is the pass's
	// own answer rather than a second rule.
	Holds        lifecyclerepo.HoldList
	Contributors lifecyclerepo.ContributorReader
}

ListTrash reads what is in the trash, newest deletion first.

One entry per deletion rather than per deleted row. A hub with two hundred entries under it went in as one act and comes back as one act (I-C2), so what is listed is the root of each deletion - the thing somebody deleted - and the batch beside it is what took the rest.

Read-only throughout, which is not a detail: the transaction may be served by a read replica (multi-tenancy.md §7), and a read that opened a write transaction would pin it to the primary.

func (ListTrash) Descriptor

func (h ListTrash) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListTrash) Execute

func (h ListTrash) Execute(
	ctx context.Context, actor appshared.ActorContext, query ListTrashQuery,
) (TrashListing, error)

Execute returns one page of the trash.

The permission check has the shape the hub level has, and for the same reason: the trash is anchored to nothing. It spans hubs, so there is no single path to check - and checking the tenant scope would refuse everybody whose membership sits on a hub rather than on the workspace, which is most of what hub-scoped memberships are for. So the page is read and then narrowed to what the actor may see: "what did I delete" is answered with what they may see, not with a 403.

type ListTrashQuery

type ListTrashQuery struct {
	Cursor string
	Size   int
}

ListTrashQuery is the input, typed.

type ListWorkItems

type ListWorkItems struct {
	Items      repository.Items
	ItemLabels repository.ItemLabels
	Containers repository.Containers
	Authorizer Anchored
	UnitOfWork persistence.UnitOfWork
}

ListWorkItems reads one level of one collection.

func (ListWorkItems) Descriptor

func (h ListWorkItems) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ListWorkItems) Execute

Execute returns one page of the level.

One permission check for the whole page, unlike the hub level of ListContainers. This list is anchored: the client named a collection, so there is a single path to ask about, and the answer covers every row in it - a membership at the collection, or at its hub, applies to everything inside. A refusal is therefore a refusal rather than an empty page, which is the honest answer when the client named the container it cannot read.

The one caller for whom that single answer is not the whole truth is somebody who holds no role on the collection and a membership on entries inside it. Their level is those entries, and the restriction goes into the query rather than filtering the page afterwards: filtered afterwards, a page would come back short and its cursor would skip.

type ListWorkItemsQuery

type ListWorkItemsQuery struct {
	// CollectionID is required. A list of every item in a tenant is an unindexed scan, and every
	// filter beyond one level is the query DSL's.
	CollectionID shared.ID
	// ParentID is the item whose children are wanted. Empty means the items directly in the
	// collection.
	ParentID        shared.ID
	IncludeArchived bool
	Cursor          string
	Size            int
}

ListWorkItemsQuery is the input, typed.

type MaterializationOutcome

type MaterializationOutcome struct {
	// Created counts the occurrences that came into being, Considered the series the pass looked
	// at - which is what tells the handler whether its batch filled.
	Created    int
	Considered int
	// NextAt is when the tenant next owes an occurrence, and nil when it owes none for now. It is
	// the moment the horizon reaches the next occurrence, not the occurrence itself: a series with
	// a ninety-day window owes tomorrow's entry ninety days before it is due.
	NextAt *time.Time
}

MaterializationOutcome is what one pass did and what it leaves behind.

type MaterializeOccurrences

type MaterializeOccurrences struct {
	Recurrences repository.Recurrences
	Items       repository.Items
	Containers  repository.Containers
	// Copy is DuplicateWorkItem, reused rather than rebuilt: an occurrence is a copy of the
	// template with its subtree, and everything that makes a copy correct - the vocabulary of the
	// destination, the references it cannot carry (I-W6), the records each entry owes - is already
	// decided there.
	Copy     DuplicateWorkItem
	Expander recurrenceport.Expander
	Events   outbox.Events
	Clock    clock.Clock
	IDs      clock.IDGenerator
	Signals  OccurrenceSignals
	// RuleBatch bounds how many series one pass looks at, OccurrenceBatch how many entries one
	// series may produce in it. A pass is one transaction, and a transaction that materialised a
	// year of everything would hold its locks for as long as that took.
	RuleBatch       int
	OccurrenceBatch int
}

MaterializeOccurrences turns the series of one tenant into the entries their rolling windows owe (arc42 §6.3).

Internal, and deliberately absent from the catalogue in domain-model.md §5 for the reason ReconcileMedia and FireReminders are absent from it: the catalogue is what a person, an agent or a rule can ask for, and "materialise everybody's series now" is not something anybody should be able to ask for. The way to influence what a series produces is the rule, and the way to skip one occurrence is SkipOccurrence.

It runs inside the transaction the job runner opened, which is what makes it exactly-once: the occurrences, their events, the watermark's compare-and-set and the job's own completion commit together. Two leaders that both wake up cannot mint the same morning twice - the second finds the watermark moved, fails the update, and rolls back the entries it wrote with it.

func (MaterializeOccurrences) Execute

Execute materialises what the tenant's series owe.

type MediaReferences

type MediaReferences interface {
	Find(ctx context.Context, id shared.ID) (media.Object, error)
	AdjustRefCount(ctx context.Context, id shared.ID, delta int) error
}

MediaReferences is the slice of the media record store this package needs: read one object, and move its reference counter.

Narrow rather than the whole port, for the reason Visibility is narrow: a use case declaring a dependency it does not use is a use case whose test has to satisfy it anyway. The counter is the only thing this package writes about a media object - what the object is, and whether it may be used at all, is the media domain's answer (media.Object.Attachable).

type MemberCommand

type MemberCommand struct {
	ItemID    shared.ID
	AccountID shared.ID
}

MemberCommand is the input of both directions, typed.

No expected version, for the reason LabelCommand spends none: a member lives beside the entry rather than on its row, so there is no version of the entry an addition would be racing. Two devices adding two different people at once is the case the OR-set exists to serve, and an optimistic lock on the entry would make one of them fail for no reason (offline-sync.md §4.2).

type MintedFeed

type MintedFeed struct {
	Feed  domain.CalendarFeed
	Token domain.FeedToken
}

MintedFeed is what a minting answers: the feed as it will be listed, and the credential that will never be readable again.

type Moderation

type Moderation interface {
	RoleAlong(
		ctx context.Context, actor appshared.ActorContext, path []identity.Scope,
	) (identity.Role, bool, error)
}

Moderation is the slice of the authorisation service the comment changes need beyond Authorizer: the actor's own effective role, for the rule the permission matrix cannot express - only the author or an administrator may change a comment (domain-model.md §3.5).

type MoveContainer

type MoveContainer struct {
	Writer ContainerWriter
	// Revocations tells the devices of whoever read the collection through the hub it left, and
	// cannot read it where it now sits, that they lost it.
	Revocations MoveRevoker
}

MoveContainer moves a collection into another hub, and ranks it there.

A hub is refused: it sits in the tenant and in nothing else (I-C1), so there is no destination to name. Naming the hub a collection already sits in reorders it there, which is the same operation with the same event - a drag within a level and a drag between hubs are one gesture to a person.

Nothing under the collection is written. A container tree is two deep, so a collection has no containers below it whose placement would have to follow, and the items it holds reference their collection rather than a path through the hubs.

func (MoveContainer) Descriptor

func (h MoveContainer) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (MoveContainer) Execute

Execute moves the collection and returns it as it now stands.

type MoveContainerCommand

type MoveContainerCommand struct {
	ContainerID shared.ID
	// TargetParentID is the hub to move into. Required, and not nullable: there is no level above a
	// hub for a collection to be moved to.
	TargetParentID shared.ID
	// BeforeContainerID is the sibling to land in front of at the destination. Empty appends.
	BeforeContainerID shared.ID
	ExpectedVersion   int
}

MoveContainerCommand is the input, typed.

type MoveResult

type MoveResult struct {
	Item domain.WorkItem
	// SubtreeSize is how many rows moved, the item included. The event reports it so that a client knows how
	// much of its own copy to rewrite.
	SubtreeSize int
	// DroppedReferences is what the destination collection could not resolve: the labels of the
	// vocabulary the subtree left, and the columns of the board it was on (I-W6). Empty rather than
	// nil, and empty for every move within one collection - nothing there has to be resolved again.
	//
	// The members and the custom fields join it with the use cases that own them.
	DroppedReferences []domain.DroppedReference
}

MoveResult is what a move answers with: the item, and the references a change of collection could not carry over (I-W6).

type MoveRevoker

type MoveRevoker interface {
	AfterContainerMoved(ctx context.Context, from, to domain.Container) (access.Loss, error)
	Announce(ctx context.Context, tenantID shared.ID, losses ...access.Loss) error
}

MoveRevoker is the slice of access.Revocations a move needs: who may have lost the collection, and the ACCESS_REVOKED record for each of them who did.

type MoveWorkItem

type MoveWorkItem struct {
	Placement PlacementWriter
}

MoveWorkItem moves an item, and its subtree with it, to a new parent or a new collection.

func (MoveWorkItem) Descriptor

func (h MoveWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4) - and a move is the action a kanban rule performs, so the automation door is not a nicety here.

func (MoveWorkItem) Execute

Execute moves the item and returns it.

type MoveWorkItemCommand

type MoveWorkItemCommand struct {
	ItemID shared.ID
	// TargetParentID is the item to move under. Meaningful only together with ParentGiven: the zero value is
	// both "the top level" and "not asked for", and those are different requests.
	TargetParentID shared.ID
	// ParentGiven says the caller named a parent, null included. Without it, a request that omitted the field
	// entirely would be read as "move to the top level" - which is how an item ends up somewhere nobody asked
	// for.
	ParentGiven bool
	// TargetCollectionID may be omitted when a parent is given: an item's collection is the one its parent is
	// in, and making a client repeat it is making it possible to contradict.
	TargetCollectionID shared.ID
	// BeforeItemID is the sibling to land in front of at the destination. Empty appends.
	BeforeItemID shared.ID
	// OrderKey is the rank a device computed itself, between the neighbours it holds, in place of
	// naming one (offline-sync.md §4.2: the position is a key between neighbours, not an integer).
	// Empty means the server computes the key from BeforeItemID. Both is a contradiction.
	OrderKey string
	// TargetBucketID is the column of the destination's board to land in, meaningful only together
	// with BucketGiven: the zero value is both "no column" and "not asked for".
	TargetBucketID  shared.ID
	BucketGiven     bool
	ExpectedVersion int
}

MoveWorkItemCommand is the input, typed.

type OccurrenceSignals

type OccurrenceSignals interface {
	OccurrenceMaterialized(ctx context.Context, mode string, lagSeconds float64)
}

OccurrenceSignals is the slice of the metrics adapter the materialisation reports through (observability-reliability.md §3.2, ADR-0008's promised occurrence lag).

One measurement: how late an occurrence was created against the moment it is for. A series whose occurrences appear the morning they are due is working; one whose lag grows is a scheduler that has stopped, and the entries a person expects to find are not there.

type OpenPrivateHub

type OpenPrivateHub struct {
	Accesses   repository.EmergencyAccesses
	Hubs       identityrepository.HubLocks
	Notices    EmergencyNotices
	Authorizer Authorizer
	// StepUp judges the fresh proof. Nil refuses rather than letting the act through unproven.
	StepUp     stepup.Verifier
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
}

OpenPrivateHub is the workspace owner's emergency access to a private hub (ADR-0073 §4): a fresh proof, a stated reason, one hour, not renewable - and every member of the hub told in the same transaction that opens it, so that nobody reads the hub before its members are told.

Only the workspace's owner - a person, never a service account or a rule - and only for a private hub they hold nothing in: a shared hub is theirs to read already, and one they hold a role in is open to them. A second access while one runs is refused, naming its end; one after it ended is a new act, told again. What it opens, and for whom, is the authoriser's (identity.md §22).

func (OpenPrivateHub) Descriptor

func (h OpenPrivateHub) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (OpenPrivateHub) Execute

Execute opens the hub and answers the access.

type OpenPrivateHubCommand

type OpenPrivateHubCommand struct {
	HubID       shared.ID
	Reason      string
	StepUpToken string
}

OpenPrivateHubCommand is the input, typed.

type OwnHubs

type OwnHubs interface {
	AuthorizeOwnPrivateHub(ctx context.Context, actor appshared.ActorContext, request access.Request) error
}

OwnHubs is the authorisation question a private hub of one's own asks instead of STRUCTURE (ADR-0073 §2).

type OwnersNotice

type OwnersNotice interface {
	PrivateHubTrashed(ctx context.Context, tenantID shared.ID, recipients []shared.ID) error
}

OwnersNotice tells the workspace's owners that a private hub went to the trash, naming nothing - the notification context's recorder.

type Ownership

type Ownership interface {
	WritesOnlyWhatIsAssigned(
		ctx context.Context, actor appshared.ActorContext, path []identity.Scope,
	) (bool, error)
}

Ownership is the slice of the authorisation service the create path needs beyond Authorizer: whether the role the actor holds writes only what is assigned to them.

Its own interface rather than a method on Authorizer, for the reason that split Visibility off: a use case declaring a dependency it does not use is a use case whose test has to satisfy it anyway, and creation is the only write that has to know this.

type Permitting

type Permitting interface {
	Permits(ctx context.Context, actor appshared.ActorContext, request access.Request) (bool, error)
}

Permitting is the slice of the authorisation service the view reads need beyond Authorizer: the same question, answered without an audited refusal - a view the actor may not see is not found, and a DENIED entry for every invisible bookmark would make the trail unreadable (T-04).

type PlacementWriter

type PlacementWriter struct {
	Items      repository.Items
	Buckets    repository.Buckets
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

PlacementWriter is what moving and reordering share.

One dependency set held by both use cases, on the reasoning that keeps them one event type: a reorder is a move that keeps its parent. What differs is how much has to be rewritten, and that is one branch rather than two implementations.

type PrivacyRevocations

type PrivacyRevocations interface {
	AfterHubMadePrivate(ctx context.Context, hub domain.Container) (access.Loss, error)
	Announce(ctx context.Context, tenantID shared.ID, losses ...access.Loss) error
}

PrivacyRevocations is the slice of access.Revocations a hub turning private needs: who held it through the workspace, and the record telling their devices (offline-sync.md §6).

type PrivateHubRow

type PrivateHubRow struct {
	repository.PrivateHubSummary
	// PurgeOn is the day a trashed hub goes for good, by the workspace's trash period.
	PurgeOn string
	// EmergencyOffered is whether the caller may open the hub through the emergency access now:
	// the workspace's owner, a person, holding nothing in it, with no access of theirs running -
	// the full condition OpenPrivateHub asks, so the control is absent wherever it would refuse.
	EmergencyOffered bool
	// OpenUntil is the end of the caller's running access, nil where none runs.
	OpenUntil *time.Time
}

PrivateHubRow is one row of the answer.

type PrivateHubsRule

type PrivateHubsRule interface {
	PrivateHubsAllowed(ctx context.Context, tenantID shared.ID) (bool, error)
}

PrivateHubsRule answers whether the workspace allows new private hubs now: *Private hubs allowed* at the workspace, the plan and the installation, resolved (ADR-0073 §6).

type QueryItems

type QueryItems struct {
	Items      repository.Items
	ItemLabels repository.ItemLabels
	Containers repository.Containers
	Authorizer Authorizer
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
}

QueryItems answers the query language: one filter over one anchored scope, sorted, paged, and optionally grouped into the columns a board draws (api-guidelines.md §3).

It lives in this package rather than in a `view` one of its own, although the catalogue groups it with the saved views. What it reads is work items, and it answers them through this package's projection, its authorisation path, its page size and its label expansion - four things a second package would have to import or copy. `view` is for SavedView, which stores a query rather than running one; the *grammar* is already in core/domain/model/view, where project-structure.md §1 puts it.

func (QueryItems) Descriptor

func (h QueryItems) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

The filter, the ordering and the grouping are declared as documents rather than as fields of their own, because they are a grammar: what may go in them is answered by `GET /meta/capabilities` under `query_fields`, which is a list this installation computes and not one a schema could pin. An agent reads that manifest for the same reason a client does (ai-first.md §1.1).

func (QueryItems) Execute

Execute answers the query.

Three steps, in this order. The placeholders are resolved first because it costs nothing and a query that cannot be resolved is one nobody should read a row for. The anchor is then read and the permission asked about it - once, for the whole result, exactly as ListWorkItems asks about the collection it was given: the client named the scope, so a refusal is a refusal rather than an empty page - and somebody who holds only individual shares inside it queries those. Only then does the compiled statement run.

type QueryMeaning

type QueryMeaning interface {
	// Of embeds the query and names the model that did. An empty vector and a nil error is
	// "search lexically", which is the answer for every reason a provider might not be reachable
	// - a search must not fail because somebody else's machine is slow. The model travels because
	// the vector is compared only with rows of the same model.
	Of(ctx context.Context, actor appshared.ActorContext, words string) (vector []float32, model string, err error)
}

QueryMeaning turns what somebody typed into a vector, or says it cannot.

A seam of its own rather than the provider, because the interesting part is not the call but the *policy around it*: whether this installation has a store at all, whether the workspace has consented, and what to do when the provider is slow. All of that is decided in one place, and what this use case receives is a vector or nothing.

type ReadCalendarFeed

type ReadCalendarFeed struct {
	Feeds    feedrepo.CalendarFeeds
	Accounts identityrepo.Accounts
	// Export is the selection the view export performs, without its audit entry: the same rows,
	// the same cap, the same visibility question. Shared rather than reimplemented, so a feed and
	// an export of one view can never disagree about what is in it.
	Export     ExportView
	UnitOfWork persistence.UnitOfWork
}

ReadCalendarFeed answers what one subscription contains.

It is not a use case and is deliberately not in the catalogue, for MediaContent's reason: a catalogue entry is something a person, an agent or a rule may ask for, and this is a route answering a credential nobody in the system holds. What it shares with a use case is the rule that matters - the tenant comes from the credential and from nowhere else (multi-tenancy.md §2.2) - and the rule that matters more: every permission question is asked here, in the application layer, and the adapter only ever parses a string (ADR-0005).

The feed reads as its **owner**, evaluated now rather than when the token was minted. An owner who has lost access to half the view sees the feed narrow with them, a disabled account's feed stops answering, and a revoked membership is not survived by a token that remembers better days.

func (ReadCalendarFeed) Execute

func (h ReadCalendarFeed) Execute(
	ctx context.Context, token domain.FeedToken,
) (ExportedView, error)

Execute answers the feed's rows, or that there is no such feed.

Every refusal is the same one. An unknown token, a revoked feed, a feed whose view was deleted, an owner who no longer exists and an owner who may no longer see the view all answer not found, with the same body: distinguishing them would answer questions for whoever is trying tokens (T-04's shape, and T-21's requirement).

type Reader

type Reader interface {
	Permitted(
		ctx context.Context, actor appshared.ActorContext, request access.Request,
		paths [][]identity.Scope,
	) ([]bool, error)
}

Reader is the slice of the authorisation service the read side needs beyond Authorizer: the same question asked of a whole page at once.

Two interfaces rather than one with two methods, because the two reads need different halves - a single Get asks about one path, and only the unanchored list needs the plural form. A use case declaring a dependency it does not use is a use case whose test has to satisfy it anyway.

type RebuildOutcome

type RebuildOutcome struct {
	// Rewritten is how many rows this pass rewrote.
	Rewritten int64
	// Remaining is how many were still stale after it, which is what the job reports as progress.
	Remaining int64
}

RebuildOutcome is what one pass did.

type RebuildSearchIndex

type RebuildSearchIndex struct {
	Index      repository.SearchIndex
	UnitOfWork persistence.UnitOfWork
	// BatchSize bounds one pass. Zero takes the default below.
	BatchSize int
}

RebuildSearchIndex is the pass behind the job: one batch of stale rows rewritten in its own transaction, and a decision about whether to come straight back.

Not a use case and not in the catalogue: nobody asks for a batch, they ask for the index to be current (ReindexSearch). The shape is the embedding pass's - a batch, then the outcome says whether there is more - with no provider anywhere near it: the rewrite is one statement over rows this PostgreSQL holds.

func (RebuildSearchIndex) Execute

Execute rewrites one batch and counts what is left.

type RecurrenceWriter

type RecurrenceWriter struct {
	Recurrences repository.Recurrences
	Items       repository.Items
	Containers  repository.Containers
	Profiles    metarepo.CapabilityProfiles
	Authorizer  Authorizer
	// Expander is the library behind ADR-0008's decision, as a port: it is what turns "is this a
	// rule, and is it a sane one" from an opinion into an answer, and it is asked before anything
	// is stored rather than by the scheduler afterwards (T-17, R-07).
	Expander recurrenceport.Expander
	Changes  changelog.ChangeLog
	Audit    audit.Sink
	Activity ActivityJournal
	// Jobs is where the series asks to be materialised. The write that made something owed seeds
	// it, because nothing may enumerate tenants (multi-tenancy.md §2.1) - the same shape the
	// reminder's wake-up has.
	Jobs       queue.Queue
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

RecurrenceWriter is what both directions of a series need: the same reads, the same permission question, the same records - and the port that decides whether the text is a rule at all.

type ReindexAccepted

type ReindexAccepted struct {
	JobID shared.ID
	Stale int64
}

ReindexAccepted is what a 202 hands back: the job to watch, and how many rows it will rewrite.

type ReindexSearch

type ReindexSearch struct {
	Index      repository.SearchIndex
	Jobs       Jobs
	Authorizer Authorizer
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
}

ReindexSearch brings a workspace's search documents current with the configurations its PostgreSQL has (ADR-0034).

ADR-0034 left one case open: an installation that *gains* a text search configuration after the entries were written - an upgrade, or an operator installing one - keeps searching those entries as `simple` until each happens to be edited. The rows now record which configuration built them, so the stale ones can be found; this counts them, queues the job that rewrites exactly those in batches, and answers the count, which is the operation's report. Tenant-scoped and an administrator's, because nothing may enumerate tenants (multi-tenancy.md §2.1).

func (ReindexSearch) Descriptor

func (h ReindexSearch) Descriptor() usecase.Descriptor

Descriptor registers the reindex in all three channels.

func (ReindexSearch) Execute

Execute counts, queues and records.

type ReminderNotifier

type ReminderNotifier interface {
	Execute(ctx context.Context, tenantID, itemID shared.ID, recipients []shared.ID) error
}

ReminderNotifier is the slice of the Notification context this pass needs: somebody is to be told about an entry, now.

A slice rather than the package, for the reason the notification consumer declares slices of the work repositories: what the firing pass can do to the notification context is one line - it can ask for a record to be written - and it cannot read, suppress or send anything.

type ReminderOutcome

type ReminderOutcome struct {
	// Fired counts the reminders that became notifications, Cancelled the ones whose entry no
	// longer warrants one, and Announced the deadlines the pass told automation about.
	Fired     int
	Cancelled int
	Announced int
	// NextAt is when the tenant next owes a reminder, and nil when it owes none - which is what
	// lets the job finish instead of idling forever.
	NextAt *time.Time
}

ReminderOutcome is what one pass did and what it leaves behind.

func (ReminderOutcome) FilledBatch

func (o ReminderOutcome) FilledBatch(batch int) bool

FilledBatch reports whether the pass ran into its own bound, in which case there is known work left and the job comes straight back for it rather than sleeping until the next moment.

type ReminderSignals

type ReminderSignals interface {
	ReminderFired(ctx context.Context, channel string, delaySeconds float64)
}

ReminderSignals is the slice of the metrics adapter this pass reports through (observability-reliability.md §3.2, SLO-5).

One measurement, and it is the one the service level objective is written about: how late a reminder was against the moment it promised. The channel is the label because that is where the difference will be once there is more than one - an email that waits for a mail server is a different story from a push that does not.

type ReminderWriter

type ReminderWriter struct {
	Reminders  repository.Reminders
	Items      repository.Items
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	// Visibility answers the one question the permission cannot: whether the account somebody
	// named can see the entry at all. The same question an assignment asks.
	Visibility Visibility
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	// Jobs is where the tenant's next wake-up is asked for. The write that makes something due is
	// what seeds it, because nothing may enumerate tenants (multi-tenancy.md §2.1) - the same
	// shape the retention sweep and the media reconciliation already have.
	Jobs       queue.Queue
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
}

ReminderWriter is what every use case that writes a reminder shares: the same reads, the same permission question, and the same two records - the change log entry for offline clients, and the audit entry.

Two records rather than four. There is no reminder event in the §4 catalogue and none is invented here: what consumers react to is the entry moving (`item.due_changed`) and the reminder firing, not somebody writing one down. And there is no step of the entry's history either: a reminder is a person's own arrangement with the clock rather than a change to the work - the history says what happened to the entry, and "Anna set herself a reminder" is not that. Both decisions are recorded in the pull request, because the next reader will ask.

type RemoveLabel

type RemoveLabel struct {
	Writer ItemLabelWriter
}

RemoveLabel takes a label off an entry.

func (RemoveLabel) Descriptor

func (h RemoveLabel) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (RemoveLabel) Execute

Execute takes the label off the entry and returns the labels it now carries.

type RemoveMember

type RemoveMember struct {
	Writer ItemMemberWriter
}

RemoveMember takes an account off an entry's member list.

func (RemoveMember) Descriptor

func (h RemoveMember) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (RemoveMember) Execute

Execute takes the account off the entry and returns the members it now carries.

type RemoveRecurrence

type RemoveRecurrence struct {
	Writer RecurrenceWriter
}

RemoveRecurrence takes the series off an entry, leaving every occurrence it produced standing.

func (RemoveRecurrence) Descriptor

func (h RemoveRecurrence) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (RemoveRecurrence) Execute

Execute takes the series off the entry.

type RemoveRecurrenceCommand

type RemoveRecurrenceCommand struct {
	ItemID          shared.ID
	ExpectedVersion int
}

RemoveRecurrenceCommand is the input, typed.

type RenameContainer

type RenameContainer struct {
	Writer ContainerWriter
}

RenameContainer changes a hub or a collection's own descriptive fields.

What it does not change: how the collection works (UpdateContainerPolicies), where it sits (MoveContainer), and whether it is writable at all (ArchiveContainer). A single endpoint writing all of them would need one audit entry covering everything and one event nobody could subscribe to narrowly.

func (RenameContainer) Descriptor

func (h RenameContainer) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (RenameContainer) Execute

Execute renames the container and returns it as it now stands.

type RenameContainerCommand

type RenameContainerCommand struct {
	ContainerID shared.ID
	// Attributes carries a pointer per field, so that "set it to nothing" and "do not touch it" stay
	// two different requests all the way down from the merge patch that expressed them.
	Attributes domain.ContainerAttributes
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read none
	// and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

RenameContainerCommand is the input, typed.

type ReopenWorkItem

type ReopenWorkItem struct {
	Completion CompletionWriter
}

ReopenWorkItem marks a completed item open again, and reopens the completed items above it where the policy rolls up.

func (ReopenWorkItem) Descriptor

func (h ReopenWorkItem) Descriptor() usecase.Descriptor

func (ReopenWorkItem) Execute

Execute reopens the item and returns it.

type ReorderBucket

type ReorderBucket struct {
	Writer BucketWriter
}

ReorderBucket moves a column along its board.

func (ReorderBucket) Descriptor

func (h ReorderBucket) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ReorderBucket) Execute

Execute moves the column and returns it as it now stands.

type ReorderBucketCommand

type ReorderBucketCommand struct {
	BucketID shared.ID
	// BeforeBucketID is the column this one goes to the left of. Empty is the right hand end, which
	// is a position like any other rather than a special case.
	BeforeBucketID  shared.ID
	ExpectedVersion int
}

ReorderBucketCommand is the input, typed.

type ReorderContainer

type ReorderContainer struct {
	Writer ContainerWriter
}

ReorderContainer ranks a container within the level it already sits in.

It exists because MoveContainer cannot express the case that matters most in a sidebar. A move requires a `target_parent_id` and ranks the collection there as a consequence - naming the hub a collection is already in reorders it, which the operation says itself. A **hub** sits in the tenant and in nothing else (I-C1), so it has no parent to name, while `domain-model.md` §3.3 defines `order_key` as "ordering within the parent context" and every Container answers one. The rank of a hub was therefore a field the API reported and nothing could change, and a sidebar listed hubs in an order their owner could not touch.

The audit action and the event are MoveContainer's rather than new ones. `container.moved` is already documented as covering "a change of hub and a reorder within one", and ContainerMoved already says a consumer recognises a reorder by `from_parent_id` matching `parent_id`. A second name for the same fact would be a second thing for every consumer to learn and for the change log to keep in step.

func (ReorderContainer) Descriptor

func (h ReorderContainer) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ReorderContainer) Execute

Execute ranks the container and returns it as it now stands.

type ReorderContainerCommand

type ReorderContainerCommand struct {
	ContainerID shared.ID
	// BeforeContainerID is the sibling to land in front of. Empty appends to the end of the level.
	BeforeContainerID shared.ID
	ExpectedVersion   int
}

ReorderContainerCommand is the input, typed.

type ReorderWorkItem

type ReorderWorkItem struct {
	Placement PlacementWriter
}

ReorderWorkItem changes an item's rank among the items it already sits with.

func (ReorderWorkItem) Descriptor

func (h ReorderWorkItem) Descriptor() usecase.Descriptor

func (ReorderWorkItem) Execute

Execute reorders the item and returns it.

A reorder is a move that keeps its parent, and it is expressed as exactly that: the same plan, with the destination read off the item rather than from the request. What it does not do is rewrite the subtree - nothing moved, so no path changed, and a statement over the subtree would spend a version on every descendant for no reason.

type ReorderWorkItemCommand

type ReorderWorkItemCommand struct {
	ItemID       shared.ID
	BeforeItemID shared.ID
	// OrderKey is the rank the caller computed, MoveWorkItemCommand's field.
	OrderKey        string
	ExpectedVersion int
}

ReorderWorkItemCommand is the input, typed.

type RestoreContainer

type RestoreContainer struct {
	Writer ContainerWriter
}

RestoreContainer takes one container deletion back out of the trash, whole.

The same permission as the deletion. Whoever owns the decision to delete a hub owns its reversal: an administrator who may not delete one may not bring one back either, or an owner's deletion would be undoable by somebody the matrix deliberately excluded from making it.

func (RestoreContainer) Descriptor

func (h RestoreContainer) Descriptor() usecase.Descriptor

func (RestoreContainer) Execute

Execute restores the deletion the container belongs to and returns it as it now stands.

type RestoreWorkItem

type RestoreWorkItem struct {
	Lifecycle LifecycleWriter
}

RestoreWorkItem takes one deletion back out of the trash, whole.

func (RestoreWorkItem) Descriptor

func (h RestoreWorkItem) Descriptor() usecase.Descriptor

func (RestoreWorkItem) Execute

Execute restores the deletion the entry belongs to and returns the entry as it now stands.

type RestrictedAccounts

type RestrictedAccounts interface {
	Restricted(ctx context.Context, accountIDs []shared.ID) (map[shared.ID]bool, error)
}

RestrictedAccounts answers which of the accounts named may not be processed automatically - the question automatic assignment already asks of its pool.

type RevokeCalendarFeed

type RevokeCalendarFeed struct{ Writer CalendarFeedWriter }

RevokeCalendarFeed stops a token.

func (RevokeCalendarFeed) Descriptor

func (h RevokeCalendarFeed) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (RevokeCalendarFeed) Execute

func (h RevokeCalendarFeed) Execute(
	ctx context.Context, actor appshared.ActorContext, feedID shared.ID,
) error

Execute revokes the feed, and is idempotent: revoking twice is somebody making sure.

The permission is the same read the minting asked for, and deliberately no more. Revocation must never be harder than minting - the moment somebody needs it is the moment a URL has gone somewhere it should not have, and a person who cannot stop their own leak because their role changed is a security problem rather than a policy.

type SavedViewCommand

type SavedViewCommand struct {
	ViewID shared.ID
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

SavedViewCommand is the shared input of the three: which view, and the version the caller read.

type SavedViewWriter

type SavedViewWriter struct {
	Views      viewrepo.SavedViews
	Containers repository.Containers
	Authorizer Authorizer
	Permits    Permitting
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	// Text brings the name to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

SavedViewWriter is what the three changes to an existing view need.

One dependency set, for the reason CustomFieldWriter is one: the same find, the same visibility, the same ownership question, and only what is written at the end differs.

type SearchItems

type SearchItems struct {
	Items      repository.Items
	Containers repository.Containers
	Authorizer Authorizer
	Anchored   Anchored
	Reader     Reader
	UnitOfWork persistence.UnitOfWork
	// Meaning is the semantic half (ADR-0050). Optional at every level - no store, no
	// provider, no consent, or a provider that does not answer - and every one of those is a
	// lexical search rather than a failure.
	Meaning QueryMeaning
	// Clock is what `@today` is resolved against, as it is for the query (rule 4, arc42 §8.13).
	// A filter arrives here since ADR-0064, and a filter may carry a placeholder.
	Clock clock.Clock
}

SearchItems answers "where is this".

The one read of this product that is not anchored to a place in the workspace. A query is anchored because an unanchored one is a question authorisation cannot answer in one step (view.Scope); a search is the question "anywhere I can see", so it is answered the way the trash and the hub level are - read, then narrowed to what the actor may see rather than refused (ListTrash, ReadContainers.Execute).

Which makes the narrowing the security-critical half of this use case rather than a detail of it. A search that returned a title from a collection the actor cannot open is T-04 with a different verb, and no scope check upstream can prevent it: the rows come from everywhere at once. It is asked here, in the application layer, like every other permission (rule 2, ADR-0005).

func (SearchItems) Descriptor

func (h SearchItems) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (SearchItems) Execute

Execute answers one page of hits, in the order the database ranked them.

type SearchItemsQuery

type SearchItemsQuery struct {
	Words string
	// ContainerID narrows the search to one hub or collection. Empty searches everywhere.
	ContainerID shared.ID
	// Language is the tag the words are read under. Empty takes the actor's locale.
	Language        string
	IncludeArchived bool
	IncludeTrashed  bool
	Cursor          string
	Size            int
	// Mode is AUTO or LEXICAL, and empty is AUTO.
	Mode string
	// Filter and Sort are the raw request, parsed here with the query's own parsers - one grammar,
	// two readers (ADR-0064). Nil is no narrowing and no ordering asked for.
	Filter any
	Sort   any
}

SearchItemsQuery is the input, typed.

type SearchMeaning

type SearchMeaning struct {
	Providers AiProviders
	Semantic  SemanticSearch
	// UnitOfWork is needed for the one read: whether this installation has a store at all.
	UnitOfWork persistence.UnitOfWork
	// Timeout bounds the provider call. Zero takes the default below.
	Timeout time.Duration
	// Kept is whose content stays out of AI. A person kept out searches by words: what they type
	// is theirs, and no AI action is theirs (UC-PRV-05 check 7). Nil keeps nobody out.
	Kept KeptOutReader
}

SearchMeaning embeds what somebody typed, where that is possible at all (ADR-0050).

**Its whole design is the answer to "and if it is not".** Semantic search is optional four times over - the extension may be absent, the workspace may have configured no provider, it may have configured one and not consented, and the provider may simply not answer today - and every one of those has the same right answer: search lexically. So this returns a vector or nothing, and never an error a caller has to interpret.

That is also why the timeout is here and short. A search is something somebody is waiting for; a provider that takes four seconds has, for the purpose of this feature, not answered. The lexical half is complete on its own, so the cost of giving up early is a search that finds slightly less, and the cost of not giving up is a search that hangs.

func (SearchMeaning) Of

func (m SearchMeaning) Of(
	ctx context.Context, actor appshared.ActorContext, words string,
) ([]float32, string, error)

Of embeds the query, or answers nothing.

type SeedEmbedding

type SeedEmbedding struct {
	Jobs EmbeddingQueue
	// Delay is how long after the write the pass runs. Short, but not zero: a burst of edits to
	// one workspace should become one pass rather than one per keystroke, and the deduplication
	// key only collapses jobs that are still pending.
	Delay time.Duration
	Clock clock.Clock
}

SeedEmbedding asks for a workspace's vectors to be brought up to date when one of its entries changes.

It is a *seed*, not the work: what it writes is one job saying "this workspace owes embeddings", deduplicated per tenant, and the pass behind that job works out which entries actually owe one. That is why an event that changed no text is harmless - the pass compares fingerprints and finds nothing.

Seeded by the write rather than by a scheduler, like every per-tenant duty in this system: nothing may enumerate tenants (multi-tenancy.md §2.1), so nothing can create one job per workspace on a timer. A workspace nobody writes in owes nothing and costs nothing.

It runs inside the dispatcher's transaction, which is the reliability argument (ADR-0007): the job and the event's consumption commit together, so an event that was seen is an event whose job exists.

func (SeedEmbedding) Deliver

func (s SeedEmbedding) Deliver(ctx context.Context, envelope event.Envelope) error

Deliver asks for a pass.

func (SeedEmbedding) Name

func (s SeedEmbedding) Name() string

Name identifies the subscriber.

func (SeedEmbedding) Wants

func (s SeedEmbedding) Wants(eventType event.Type) bool

Wants reports whether this event could have changed an entry's text.

The two that carry a title or notes. A deletion is deliberately absent: the store's rows go with the entry by cascade, so nothing has to be asked for. So is everything else an entry can have happen to it - a label, an assignment, a move - because none of them changes the words the index is built from, and asking for a pass after each would be a provider call for a card somebody dragged.

type SemanticSearch

type SemanticSearch interface {
	Available(ctx context.Context) (bool, error)
}

SemanticSearch reports whether this installation has an embedding store at all (ADR-0050).

type SetCover

type SetCover struct {
	Cover CoverWriter
}

SetCover puts a cover on an entry: a colour token, or an image.

Only a type whose profile carries COVER has one, which by default is a task and neither a work package nor an activity (domain-model.md §2, §3.4). Refused rather than quietly ignored: a client that covered a work package and received a 200 would believe the picture is there.

Replacing a cover is this call again. The displaced image loses a reference and is left to the reconciliation job, which is the whole of the deletion path here - a request has no business waiting on a bucket for a picture somebody just swapped out.

func (SetCover) Descriptor

func (h SetCover) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (SetCover) Execute

func (h SetCover) Execute(
	ctx context.Context, actor appshared.ActorContext, cmd CoverCommand,
) (domain.WorkItem, error)

Execute puts the cover on the entry and returns it.

type SetCustomField

type SetCustomField struct {
	Items      repository.Items
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Fields     repository.CustomFields
	Authorizer Authorizer
	// Visibility answers the one question a USER value asks: can the account named reach the
	// entry at all. The same question an assignment asks, and the same refusal.
	Visibility Visibility
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Text brings the options and a TEXT value to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

SetCustomField writes one custom field on one entry.

One key per call, and that is the merge rule made unavoidable rather than a convenience. The values live in one jsonb document on the entry, and merging that document as one scalar is exactly the loss the per-field rule exists to prevent: two devices setting two different keys would resolve to whichever wrote later, and one of the two values would quietly not be there (offline-sync.md §4.2). A call that could write the whole document would be writing keys it never read, so there is no such call.

The value is judged against the definition in force for the entry's collection: the collection's own, or the workspace-wide one. Judged rather than coerced - a NUMBER arriving as a string is refused, because a client that sent "3" meant a string (domain-model.md §6).

func (SetCustomField) Descriptor

func (h SetCustomField) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (SetCustomField) Execute

Execute writes the field and returns the entry as it now stands.

type SetCustomFieldCommand

type SetCustomFieldCommand struct {
	ItemID shared.ID
	Key    string
	// Value is what the field shall hold, shaped by the definition's kind. Nil clears the key.
	Value any
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

SetCustomFieldCommand is the input, typed.

type SetDueDate

type SetDueDate struct {
	Due DueDateWriter
}

SetDueDate puts a due date on an entry: the instant, the all-day flag and the IANA zone, written together because the three describe one date (i18n-l10n.md §4).

Moving a due date is this use case again: the trio is a scalar family, so it is one decision, one version and one step of the history.

func (SetDueDate) Descriptor

func (h SetDueDate) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (SetDueDate) Execute

Execute puts the due date on the entry and returns it.

type SetHubPrivacy

type SetHubPrivacy struct {
	Writer ContainerWriter
	// Hubs takes the hub's row before anything is decided, as granting a group does: a group
	// cannot slip in while the hub turns private (identity.md §22).
	Hubs        identityrepository.HubLocks
	Revocations PrivacyRevocations
	// PrivateHubs is the workspace's switch: read when a shared hub is to turn private, never when
	// a private one is shared again (ADR-0073 §6).
	PrivateHubs PrivateHubsRule
}

SetHubPrivacy makes an existing hub private, or shared again (ADR-0073 §2).

Marking needs both the right to create hubs - STRUCTURE on the workspace - and an OWNER membership on the hub itself; a role held on the workspace does not make anybody the owner of a hub, so an administrator cannot take a shared hub away from everybody else. Unmarking needs an OWNER on the hub. A person without STRUCTURE has a private hub by creating one.

func (SetHubPrivacy) Descriptor

func (h SetHubPrivacy) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (SetHubPrivacy) Execute

Execute changes the hub's privacy and returns it as it now stands.

type SetHubPrivacyCommand

type SetHubPrivacyCommand struct {
	ContainerID     shared.ID
	Private         bool
	ExpectedVersion int
}

SetHubPrivacyCommand is the input, typed.

type SetRecurrence

type SetRecurrence struct {
	Writer RecurrenceWriter
}

SetRecurrence puts a series on an entry, or changes the one it has.

One use case for both, which is the one place this task departs from the catalogue's wording: §5 names `SetRecurrence` and `UpdateRecurrence`, and they are the same call here for the reason the route is one PUT - a series is one document, "every Monday in Berlin, on completion, ninety days ahead" is a sentence rather than six settings, and a caller sending it does not know or care whether the entry already had one. What differs between the two is the trail: the audit entry and the history verb say which of the two happened, decided from what was there.

func (SetRecurrence) Descriptor

func (h SetRecurrence) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (SetRecurrence) Execute

Execute stores the series and returns it.

type SetRecurrenceCommand

type SetRecurrenceCommand struct {
	ItemID shared.ID
	Spec   domain.RecurrenceSpec
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

SetRecurrenceCommand is the input, typed.

type ShareSavedView

type ShareSavedView struct {
	Writer SavedViewWriter
}

ShareSavedView decides who sees a view: the owner alone, or everyone who may read its scope.

func (ShareSavedView) Descriptor

func (h ShareSavedView) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (ShareSavedView) Execute

Execute decides the sharing and returns the view.

Publishing into the scope asks STRUCTURE on the scope's path, whoever asks - a view over a collection cannot be pushed onto people by somebody who may not shape that collection, the owner included. Taking a view back to PRIVATE is the owner's right without any permission, and a STRUCTURE holder's for a view that was published.

type SkipOccurrence

type SkipOccurrence struct {
	Writer RecurrenceWriter
}

SkipOccurrence moves a series past its next unmade occurrence.

The one user-facing half of the materialisation, and the shape of it is what the bookkeeping already is: the watermark says how far the series has been dealt with, so skipping is moving it past one moment without creating anything. Nothing that already exists is touched - a materialised occurrence is an ordinary entry, and skipping it would be deleting somebody's work.

func (SkipOccurrence) Descriptor

func (h SkipOccurrence) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (SkipOccurrence) Execute

Execute skips the next occurrence and answers the rule.

type SkipOccurrenceCommand

type SkipOccurrenceCommand struct {
	ItemID shared.ID
}

SkipOccurrenceCommand is the input, typed.

type TemplateWriter

type TemplateWriter struct {
	Templates  repository.Templates
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// Text brings a template's name, its description and every node's title and notes to normal
	// form C on the way in, and the entries an instantiation writes with them (i18n-l10n.md §5).
	Text text.Normalizer
	// Holds, Removals and TombstoneWindow are the deletion's (data-retention.md §4): a template no
	// legal hold covers is removed at once, with the journal entry and the tombstone every removal
	// leaves; one a hold covers is only marked deleted, for the retention pass to remove later.
	Holds           lifecyclerepo.LegalHolds
	Removals        lifecyclerepo.Removals
	TombstoneWindow time.Duration
}

TemplateWriter is what every use case that writes a template shares: the same scope resolution, the same permission question, and the same records.

The permission is STRUCTURE rather than WRITE_ITEMS, and that is the decision the backlog left open ("who may define at which scope is the application layer's question"): a template is vocabulary for a whole workspace or a whole collection, like a label or a board column, and defining one is shaping how everybody there works. Which scope somebody may shape follows the role matrix at that scope - so a workspace-wide template needs STRUCTURE at the tenant, which is an administrator, and a collection's needs it there.

type TranslateCommand

type TranslateCommand struct {
	ItemID shared.ID
	// TargetLocale is BCP 47. Empty takes the caller's own.
	TargetLocale string
}

TranslateCommand is the input, typed.

type Translation

type Translation struct {
	TargetLocale   string
	SourceLanguage string
	Title          string
	Notes          string
	Model          string
	PromptID       string
	PromptVersion  string
	ProducedAt     time.Time
}

Translation is the answer: the two texts and the provenance every AI output carries.

type TrashContainer

type TrashContainer struct {
	Writer ContainerWriter
}

TrashContainer moves a hub or a collection and everything under it into the trash.

The whole subtree, under one batch: a hub takes its collections, and every collection takes its entries (I-C2). That is what makes the way back a single decision rather than a walk that has to guess what belonged together - and it is why this is the one operation an administrator may not perform. Deleting a container takes work nobody was asked about with it, so it is the owner's decision (domain-model.md §3.2).

func (TrashContainer) Descriptor

func (h TrashContainer) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (TrashContainer) Execute

Execute puts the container and its subtree into the trash and returns it as it now stands.

type TrashListing

type TrashListing struct {
	repository.TrashPage
	// contains filtered or unexported fields
}

TrashListing is one page of the trash, when the automatic pass may take each entry, and which a legal hold keeps.

func (TrashListing) Held

func (l TrashListing) Held(entry domain.TrashEntry) bool

Held reports whether a legal hold keeps the entry's deletion: a hold blocks one of its rows, and the pass keeps a root while anything below it stays (data-retention.md §4 item 6).

func (TrashListing) PurgeAfter

func (l TrashListing) PurgeAfter(entry domain.TrashEntry) time.Time

PurgeAfter is the moment after which the automatic pass may remove the entry, unless a legal hold keeps it.

type TrashWorkItem

type TrashWorkItem struct {
	Lifecycle LifecycleWriter
}

TrashWorkItem moves an entry and everything under it into the trash.

A soft delete: the rows stay, stamped with when they went and with the batch that names this one deletion (I-C2). What removes them for good is the retention job, once the tenant's period has run - and until then a restore brings back exactly this batch and nothing else.

The subtree goes with it because a deletion that left an entry's children behind would leave them unreachable rather than deleted: their parent is gone from every list they would be read through.

func (TrashWorkItem) Descriptor

func (h TrashWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (TrashWorkItem) Execute

Execute puts the entry and its subtree into the trash and returns the entry as it now stands.

type UnarchiveContainer

type UnarchiveContainer struct {
	Writer ContainerWriter
}

UnarchiveContainer makes a hub or a collection writable again.

func (UnarchiveContainer) Descriptor

func (h UnarchiveContainer) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (UnarchiveContainer) Execute

Execute unarchives the container and returns it as it now stands.

type UnarchiveWorkItem

type UnarchiveWorkItem struct {
	Lifecycle LifecycleWriter
}

UnarchiveWorkItem makes an archived entry writable again.

func (UnarchiveWorkItem) Descriptor

func (h UnarchiveWorkItem) Descriptor() usecase.Descriptor

func (UnarchiveWorkItem) Execute

Execute unarchives the entry and returns it as it now stands.

type UnassignWorkItem

type UnassignWorkItem struct {
	Assignment AssignmentWriter
}

UnassignWorkItem takes the assignee off an entry.

func (UnassignWorkItem) Descriptor

func (h UnassignWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (UnassignWorkItem) Execute

Execute takes the assignee off the entry and returns it.

type UpdateBucket

type UpdateBucket struct {
	Writer BucketWriter
}

UpdateBucket changes a column's own fields.

What it does not change: where the column sits, which is ReorderBucket. A single endpoint writing both would need one audit entry covering everything and one event nobody could subscribe to narrowly.

func (UpdateBucket) Descriptor

func (h UpdateBucket) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (UpdateBucket) Execute

Execute changes the column and returns it as it now stands.

type UpdateBucketCommand

type UpdateBucketCommand struct {
	BucketID shared.ID
	// Attributes carries a pointer per field, so that "set it to nothing" and "do not touch it" stay
	// two different requests all the way down from the merge patch that expressed them.
	Attributes domain.BucketAttributes
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read none
	// and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

UpdateBucketCommand is the input, typed.

type UpdateCommand

type UpdateCommand struct {
	ItemID shared.ID
	// Attributes carries a pointer per field, so that "set it to nothing" and "do not touch it" stay
	// two different requests all the way down from the merge patch that expressed them.
	Attributes domain.ItemAttributes
	// Due is the patch's touch on the due trio, nil when it left the trio alone. Raw rather than
	// resolved, because the target is only decidable against the stored trio, which is read
	// inside the transaction.
	Due *domain.DuePatch
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read none
	// and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

UpdateCommand is the input, typed.

type UpdateContainerPolicies

type UpdateContainerPolicies struct {
	Writer ContainerWriter
}

UpdateContainerPolicies replaces a collection's policies.

func (UpdateContainerPolicies) Descriptor

Descriptor is the catalogue entry.

func (UpdateContainerPolicies) Execute

Execute replaces the policies and returns the collection as it now stands.

type UpdateContainerPoliciesCommand

type UpdateContainerPoliciesCommand struct {
	ContainerID shared.ID
	// Policies is the whole document rather than the keys that were sent. This is a PUT: a key the
	// caller omitted is the default, not what happens to be stored.
	Policies        domain.ContainerPolicies
	ExpectedVersion int
}

UpdateContainerPoliciesCommand is the input, typed.

type UpdateCustomField

type UpdateCustomField struct {
	Writer CustomFieldWriter
}

UpdateCustomField changes what a definition permits.

func (UpdateCustomField) Descriptor

func (h UpdateCustomField) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (UpdateCustomField) Execute

Execute applies the change and returns the definition as it now stands.

type UpdateCustomFieldCommand

type UpdateCustomFieldCommand struct {
	FieldID    shared.ID
	Attributes domain.CustomFieldAttributes
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

UpdateCustomFieldCommand is the input, typed. A pointer per field, so that "set it to nothing" and "do not touch it" stay two different requests all the way down from the merge patch that expressed them.

type UpdateLabel

type UpdateLabel struct {
	Writer LabelWriter
}

UpdateLabel changes a label's own fields.

func (UpdateLabel) Descriptor

func (h UpdateLabel) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (UpdateLabel) Execute

Execute changes the label and returns it as it now stands.

type UpdateLabelCommand

type UpdateLabelCommand struct {
	LabelID shared.ID
	// Attributes carries a pointer per field, so that "set it to nothing" and "do not touch it"
	// stay two different requests all the way down from the merge patch that expressed them.
	Attributes domain.LabelAttributes
	// ExpectedVersion is the version the caller read, from If-Match. Zero means the caller read
	// none and accepts whatever is there (api-guidelines.md §5).
	ExpectedVersion int
}

UpdateLabelCommand is the input, typed.

type UpdateReminder

type UpdateReminder struct {
	Writer ReminderWriter
}

UpdateReminder changes a reminder that has not fired yet.

func (UpdateReminder) Descriptor

func (h UpdateReminder) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (UpdateReminder) Execute

Execute changes the reminder and returns it.

type UpdateSavedView

type UpdateSavedView struct {
	Writer SavedViewWriter
}

UpdateSavedView changes a view's own fields: its name, its layout, its query, its hints.

func (UpdateSavedView) Descriptor

func (h UpdateSavedView) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (UpdateSavedView) Execute

Execute applies the update and returns the view as it now stands.

type UpdateTemplate

type UpdateTemplate struct {
	Writer TemplateWriter
}

UpdateTemplate changes a template's name, description or tree.

func (UpdateTemplate) Descriptor

func (h UpdateTemplate) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry.

func (UpdateTemplate) Execute

Execute changes the template and returns it.

type UpdateWorkItem

type UpdateWorkItem struct {
	Items      repository.Items
	Buckets    repository.Buckets
	Containers repository.Containers
	Profiles   metarepo.CapabilityProfiles
	Authorizer Authorizer
	Events     outbox.Events
	Changes    changelog.ChangeLog
	Audit      audit.Sink
	Activity   ActivityJournal
	UnitOfWork persistence.UnitOfWork
	Clock      clock.Clock
	IDs        clock.IDGenerator
	HLC        clock.HLCSource
	// DueDates is the writer the patch dispatches the due trio into.
	DueDates DueDateWriter
	// Text brings a changed title or notes to normal form C on the way in (i18n-l10n.md §5).
	Text text.Normalizer
}

UpdateWorkItem changes an item's own fields: what it is called, and what is noted on it.

The capability profile is the gate. Notes on an ACTIVITY come back as `capability_not_supported` naming the type and the capability rather than being quietly dropped - silent ignoring is how a client comes to believe it stored something (domain-model.md §2, ADR-0006).

What it does not change: where the item sits (MoveWorkItem), whether it is done (CompleteWorkItem), which labels it carries (AddLabel - a set is not a field), and the assignee, which is an action route because two ways to write that column would be two places deciding whether the person may see the entry. The due date is not on that list, in a particular way: the contract carries the three due fields on this schema, so the patch serves them - by dispatching into the writer the SetDueDate pair owns, which is how the fields keep one validation, one event and one history whichever door they arrive through.

func (UpdateWorkItem) Descriptor

func (h UpdateWorkItem) Descriptor() usecase.Descriptor

Descriptor is the catalogue entry. Registering it is what makes the use case reachable through REST, MCP and automation at once (arc42 §4).

func (UpdateWorkItem) Execute

Execute applies the update and returns the item as it now stands.

type Visibility

type Visibility interface {
	CanSee(
		ctx context.Context, actor appshared.ActorContext, accountID shared.ID,
		path []identity.Scope,
	) (bool, error)
}

Visibility is the slice of the authorisation service this package needs beyond Authorizer: the same membership question asked about somebody other than the actor.

Its own interface rather than a second method on Authorizer, on the reasoning that split Reader off: a use case declaring a dependency it does not use is a use case whose test has to satisfy it anyway, and most writers here never ask about a second person.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL