Documentation
¶
Overview ¶
Package usecase is the catalogue: every use case the system has, once.
The point is parity. A feature is registered here, and it is thereby reachable through REST, through MCP, and as an automation action - not because three teams remembered, but because the three channels are built from this one list (arc42 §4, ADR-0012, automation.md §1.3). The alternative, which every system that grew a second interface has lived through, is an API that can do things the agent interface cannot and an automation engine that lags both.
The three channel identities are derived from the use case name rather than declared. A name that could be spelled differently per channel is a name that eventually is - and then the parity test compares two lists that were never the same list.
Index ¶
- Constants
- type ActivityDeclaration
- type AuditDeclaration
- type Descriptor
- type Field
- type Format
- type Handler
- type HandlerFunc
- type Input
- func (in Input) Bool(field string) bool
- func (in Input) ID(field string) (shared.ID, error)
- func (in Input) IDList(field string) ([]shared.ID, error)
- func (in Input) Int(field string) int
- func (in Input) OptionalString(field string) *string
- func (in Input) Present(field string) bool
- func (in Input) String(field string) string
- func (in Input) StringList(field string) ([]string, error)
- type Kind
- type Middleware
- type Output
- type Registry
- func (r *Registry) All() []Descriptor
- func (r *Registry) ByAutomationAction(kind string) (Descriptor, bool)
- func (r *Registry) ByMCPTool(tool string) (Descriptor, bool)
- func (r *Registry) Invoke(ctx context.Context, name string, actor appshared.ActorContext, in Input) (Output, error)
- func (r *Registry) Lookup(name string) (Descriptor, bool)
Constants ¶
const AgentDestructiveScope = "agent:destructive"
AgentDestructiveScope is the explicit permission an agent token needs before it may call anything destructive (ai-first.md §1.3, ADR-0012).
A scope rather than a field of its own, because a scope is what this system already means by "what this credential may do, independently of the role its owner holds": it is set when the token is minted, it is visible whenever the token is read, and minting is audited. A second mechanism beside scopes would be a second place to look when answering "what can this credential do", which is the question an incident asks first.
It is a capability rather than an operation, which is why it is not derived from a descriptor like every other scope - `catalogue.Scopes` names it explicitly and says so there.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ActivityDeclaration ¶
type ActivityDeclaration struct {
Verb activity.Verb
// Exempt is why this use case writes no history entry. Empty where it writes one.
Exempt string
}
ActivityDeclaration is what the item's own history records for this use case (domain-model.md §3.5, audit.md §1).
Every work-management use case that writes declares one of the two, and the architecture gate refuses a third state. A verb is what the history records; an exemption is a reason, in prose, that this change leaves no step in an item's history - and it is a field rather than a list in a test file because the reason belongs where the use case is, next to the person adding the next one.
type AuditDeclaration ¶
type AuditDeclaration struct {
Action audit.Action
TargetType string
Severity audit.Severity
Required bool
}
AuditDeclaration is what the trail records for this use case (audit.md §7).
A use case with security or privacy relevance and no declaration fails the build (gate SG-13), which is why Required is stated rather than inferred: "does this matter to an auditor" is a judgement, and one that has to be made deliberately per use case.
type Descriptor ¶
type Descriptor struct {
// Name is the stable use case name in PascalCase, as the catalogue
// (core/application/catalogue) registers it: CreateContainer. Every channel identity is
// derived from it.
Name string
// Summary and SideEffects are what an agent reads before deciding to call the tool
// (ai-first.md §1.1). English prose, and protocol documentation rather than display text -
// like the descriptions in api/openapi.yaml, it never reaches an end user's screen.
Summary string
SideEffects string
// TokenScope is the scope a credential needs (api-guidelines.md §7). Empty for an operation
// that needs none.
TokenScope string
// AlsoScope is a narrower scope that admits this operation as well, empty for none. It exists
// for the control plane's one subset, `admin:provisioning` (api-guidelines.md §7): a purchase
// platform reaches the verbs it needs without the rest of the installation. Declared here
// rather than listed beside the catalogue, so that the scope names exactly the operations that
// check it.
AlsoScope string
// ReadOnly and Destructive become the MCP annotations `readOnlyHint` and `destructiveHint`,
// so that an agent client can ask for confirmation before the dangerous ones (ai-first.md).
ReadOnly bool
Destructive bool
// StepUp says when this operation demands a fresh re-authentication (security.md §5),
// in prose, the way Activity.Exempt gives a reason: "always", or the condition - "granting
// or revoking the OWNER role". Empty for the operations that never do. The declaration is
// what the architecture test reconciles against the privileged list, so the next privileged
// action ships with one or fails the build.
StepUp string
// Entitlement is the feature a plan may leave out that this operation uses - creating,
// changing, switching on, running or sending (multi-tenancy.md §4.2). The registry's middleware
// refuses it where the caller's workspace's plan leaves the feature out, and the rule editor is
// not offered it as an action there. Empty for every operation a plan cannot reach, which is
// every read and every act that stops or removes.
Entitlement entitlement.Feature
Input []Field
Audit AuditDeclaration
Activity ActivityDeclaration
Handler Handler
}
Descriptor is one entry of the catalogue.
func (Descriptor) AutomationAction ¶
func (d Descriptor) AutomationAction() string
AutomationAction is the action kind of the rule engine: the name in SCREAMING_SNAKE_CASE (automation.md §1.3).
func (Descriptor) MCPTool ¶
func (d Descriptor) MCPTool() string
MCPTool is the tool name: the name in snake_case (ai-first.md §1.1).
func (Descriptor) PermitAgent ¶
func (d Descriptor) PermitAgent(actor appshared.ActorContext) error
PermitAgent applies ai-first.md §1.3's first bullet: **destructive operations are blocked by default for agent tokens and must be enabled explicitly.**
Here rather than in an adapter, because it is an authorisation decision (rule 2, ADR-0005) - and here rather than in each destructive use case, because the whole point is that it holds for every one of them including the next one somebody writes. `Registry.Invoke` is the single door all three channels come through, so a use case cannot be reached with this skipped.
It refuses by naming what to change. A refusal an operator reads as a bug is a refusal that gets worked around; one that says "this token needs `agent:destructive`" is one they can act on.
Nothing here asks whether the *person* behind the token may do it. That question is the authorisation service's and is asked as it always was - this is a second, independent bound, and a narrower one, in exactly the way a token scope is (ADR-0005).
func (Descriptor) RESTOperation ¶
func (d Descriptor) RESTOperation() string
RESTOperation is the operationId in api/openapi.yaml: the name in lowerCamelCase.
func (Descriptor) ValidateInput ¶
func (d Descriptor) ValidateInput(in Input) error
ValidateInput checks an input against the declared fields. The registry calls it before every invocation, so a handler can trust the shape of what it is given.
type Field ¶
type Field struct {
Name string
Kind Kind
Required bool
Enum []string
Description string
// CallerOnly marks a field as the caller's plumbing rather than a decision of the use case's:
// the identifier a client minted for offline work, the version it last read for If-Match, a
// reserved switch. A person typing a call sets these; a rule never does, because the run has
// no version it read and mints nothing. The manifest carries it as `rule: false`, so
// that a rule editor hides the field without a list of names compiled into it; the registry
// validates the field exactly as before.
CallerOnly bool
// Format names the shape of a string field where a form can draw it better than as text:
// FormatDateTime for an RFC 3339 instant. Empty for a string that is just a string.
Format Format
}
Field declares one input field of a use case.
Description is English prose, and that is not a breach of the "no display text in the backend" rule (ADR-0011): it is protocol documentation, like the descriptions in api/openapi.yaml, read by an agent deciding whether to call the tool rather than rendered to a user. Nothing here ever reaches an end user's screen - what does is a message code.
type Format ¶
type Format string
Format is the shape of a string field, in the vocabulary of the OpenAPI `format` keyword so that the manifest and the contract say the same word.
type Handler ¶
type Handler interface {
Invoke(ctx context.Context, actor appshared.ActorContext, in Input) (Output, error)
}
Handler executes one use case. Every channel calls this, so the authorisation check, the audit entry, the event and the change log happen once, in the application layer, whichever way the call arrived (audit.md §7, test AT-6).
type HandlerFunc ¶
HandlerFunc adapts a function to Handler.
func (HandlerFunc) Invoke ¶
func (f HandlerFunc) Invoke(ctx context.Context, actor appshared.ActorContext, in Input) (Output, error)
type Input ¶
Input is what a channel hands a use case, and Output what it gets back.
Maps rather than a type per use case, because three channels have to speak the same shape: REST decodes a request body, MCP receives tool arguments, and an automation action carries parameters - all three arrive as untyped data, and a typed struct in the middle would need a mapper per use case per channel. The shape is not free-form for it: every use case declares its fields (see Field), and the registry checks the input against that declaration before a handler sees it.
func (Input) IDList ¶
IDList returns a declared list of identifiers, empty when absent. A malformed member fails the whole list: half a set of members is not a smaller request, it is a different one.
func (Input) Int ¶
Int returns a declared integer field, 0 when absent. JSON numbers arrive as float64, which is why the conversion is not a single type assertion.
func (Input) OptionalString ¶
OptionalString distinguishes an absent field from an empty one, which String deliberately does not.
For most fields the distinction is noise - a description sent as "" means what an omitted one means. For a preference it is the whole meaning: omitted is "leave my time zone alone", empty is "clear it, the workspace default applies again". A client that had only String would have to send every preference back to change one, and the day it got that wrong it would silently reset somebody's locale.
func (Input) Present ¶
Present reports whether the caller sent the field at all - the distinction OptionalString makes for strings, for the fields where the value itself carries no "absent" spelling. An empty list is a legitimate instruction (empty the group); a missing one is not an instruction.
func (Input) String ¶
String returns a declared string field. Missing and empty are the same answer for an optional field: a client that sends `""` for a description means the same thing as one that omits it.
func (Input) StringList ¶
StringList returns a declared list of strings, empty when absent. A member that is not a string fails the whole list, for the reason IDList's does: half a list is not a smaller request, it is a different one.
The same generosity about a bare value: a single string where a list belongs is the mistake every channel's caller writes at least once, and it means exactly the one-element list.
type Kind ¶
type Kind string
Kind is the type of a declared field. Deliberately few: these are the shapes a JSON Schema, an OpenAPI body and a CEL expression can all express without a translation table.
const ( KindString Kind = "string" // KindID is a UUID. Its own kind rather than a string with a pattern, so that every channel // rejects a malformed identifier the same way. KindID Kind = "id" KindBool Kind = "boolean" KindInt Kind = "integer" // KindIDList is a set of identifiers - the members of a group, the items of a bulk operation. // Its own kind for the same reason KindID is: every channel has to reject a malformed member // of the list identically, and a JSON Schema, an OpenAPI array and a CEL list all express it // without a translation table. KindIDList Kind = "id_list" // KindObject and KindList are nested documents: the filter tree of a query and its ordering. // // They are the exception to "deliberately few", and only one use case has them. A filter is a // grammar rather than a field list - what may go in it depends on what this installation // serves, and is answered by /meta/capabilities rather than pinned by a schema - so the // catalogue checks that a document arrived and leaves what is in it to the grammar that owns // it (core/domain/model/view, ADR-0026). KindObject Kind = "object" KindList Kind = "list" // KindAny is a value whose shape another declaration decides. // // The second exception, and one use case has it: a custom field's value is a text, a number, a // boolean, a date or a list depending on the definition the key names, and that definition is // data a tenant wrote rather than anything the catalogue can pin (domain-model.md §6). // Declaring it as a string would make every other kind a type error before the definition was // even read, and declaring five fields would be five ways to send one value. The catalogue // therefore checks that something arrived and leaves the judgement to the definition, exactly // as it leaves a filter tree to the grammar that owns it. KindAny Kind = "any" )
type Middleware ¶
type Middleware func(descriptor Descriptor, next Handler) Handler
Middleware wraps every handler on the way into the registry. It is how observability becomes structural rather than remembered: the composition root passes one that produces the metric and the span, and no entry in the catalogue can then be executed without them (gate RT-12).
type Output ¶
Input is what a channel hands a use case, and Output what it gets back.
Maps rather than a type per use case, because three channels have to speak the same shape: REST decodes a request body, MCP receives tool arguments, and an automation action carries parameters - all three arrive as untyped data, and a typed struct in the middle would need a mapper per use case per channel. The shape is not free-form for it: every use case declares its fields (see Field), and the registry checks the input against that declaration before a handler sees it.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the catalogue itself: immutable once built, because a catalogue that can grow at run time is one where the parity test proves nothing about what is actually running.
func NewRegistry ¶
func NewRegistry(observe Middleware, descriptors ...Descriptor) (*Registry, error)
NewRegistry builds the catalogue and refuses an incomplete entry.
The refusal happens at startup, in the composition root, so a use case missing its audit declaration or its handler stops the process rather than being discovered by whoever needed the audit entry (fail closed, ADR-0015).
func (*Registry) All ¶
func (r *Registry) All() []Descriptor
All returns every entry, in a stable order.
func (*Registry) ByAutomationAction ¶
func (r *Registry) ByAutomationAction(kind string) (Descriptor, bool)
ByAutomationAction finds the entry an automation action names.
func (*Registry) ByMCPTool ¶
func (r *Registry) ByMCPTool(tool string) (Descriptor, bool)
ByMCPTool finds the entry an MCP tool call names.
func (*Registry) Invoke ¶
func (r *Registry) Invoke(ctx context.Context, name string, actor appshared.ActorContext, in Input) (Output, error)
Invoke runs a use case by name, after checking the input against its declaration.
A name that is not in the catalogue is a not-found rather than an internal error: for MCP and automation it comes from a client naming a tool or an action, and a client's mistake is not the server's defect.