Help us shape the future of Go! Take the 2026 Go Developer Survey

catalog

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package catalog holds the product catalogue a self-service portal orders from: what may be ordered, how the parts of a product relate, and what a release proves before anything can be ordered against it.

It is the first of the three models in ADR-0312. Design-time state throughout — nothing here reaches the event log, the processor or recovery, and none of the six engine invariants applies to it. What does apply is I5: everything provable when a catalogue is published is proved there, so ordering never interprets a graph (see Publish).

Index

Constants

View Source
const (
	// EffectProvision grants: pending → running → done.
	EffectProvision = "provision"
	// EffectDeprovision revokes: done → returning → returned.
	EffectDeprovision = "deprovision"
	// EffectChange changes the configuration of what is held; the position stays
	// done, and neither the item nor the variant changes (ADR-0359).
	EffectChange = "change"
	// EffectService changes nothing about what is held: the process does something
	// for the holder — a password reset, an inactivation (ADR-0429 §10).
	EffectService = "service"
)

The effects an action can have on the position it is asked of. Closed, because the order layer has to interpret each one.

View Source
const (
	ActionProvision   = OpProvision
	ActionDeprovision = OpDeprovision
	// ActionChange is the key the legacy change route addresses (ADR-0428).
	ActionChange = OpChange
)

The keys of the two actions the order itself runs. They are reserved for the two effects, so trigger ids and recorded instances keep the names ADR-0425 gave them.

View Source
const (
	// TriggerCustomer is the orderer, the recipient who holds the right, or an
	// operator acting for either (ADR-0429 §10, decision 4).
	TriggerCustomer = "customer"
	// TriggerOperator is somebody running the service.
	TriggerOperator = "operator"
	// TriggerSystem is something observed about the held right, not a person: a
	// threshold crossed, an expiry near (ADR-0429 §1).
	TriggerSystem = "system"
)

Who may ask for an action. Closed for the same reason the effects are.

View Source
const (
	OutcomeCompleted = "completed"
	OutcomeRejected  = "rejected"
	OutcomeFailed    = "failed"
)

How a command can end. Each outcome is a fact, published under the event type the action declares for it (ADR-0429 §3).

View Source
const (
	OpProvision   = "provision"
	OpChange      = "change"
	OpDeprovision = "deprovision"
)

The operations a lifecycle process can offer.

View Source
const (
	// FormPerOperation starts an instance for every operation (ADR-0425). It is
	// what an empty LifecycleForm means.
	FormPerOperation = "per-operation"
	// FormPerPosition starts one instance per order position at provisioning and
	// delivers every later operation to it.
	FormPerPosition = "per-position"
)

The instance forms of a lifecycle process (ADR-0428).

View Source
const FulfilmentProcess = "atlas-auftrag-erfuellung"

FulfilmentProcess is the id of the model that works an order (api/systemprocesses/auftrag-erfuellung.bpmn). It lives here, where a product's bindings are checked against it, and the order package names it through this constant, so the Go side has one spelling of it.

View Source
const OrderProcess = "atlas-auftrag-erfuellung"

OrderProcess is the orchestration every order goes through. It is named here rather than imported so this package does not depend on the order package for one string; the guard in fulfilmentreport_test.go holds the two together.

View Source
const ProblemMayNotDeploy = "deploying a document's processes needs the modeler role"

ProblemMayNotDeploy is the problem a document carrying processes is refused with when its importer may not deploy them, which the response answers 403.

Variables

View Source
var OrderVariables = map[string]bool{
	"orderId": true, "itemId": true, "positionId": true, "variantId": true,
	"recipient": true, "orderer": true, "provisionProcess": true, "approvalRef": true,
	"atlasApiBase": true, "portalBaseUrl": true, "commandId": true, "reason": true,
}

OrderVariables are the variables the order gives the processes it starts for a position. A configuration form's field may not be named after one.

RecheckInterval is the sweep's period and the age a passed recheck may reach before publishing refuses (§ 9).

View Source
var Typefaces = map[string]string{
	"system":   `system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`,
	"humanist": `"Segoe UI", Candara, Optima, "Trebuchet MS", sans-serif`,
	"serif":    `Georgia, Cambria, "Times New Roman", serif`,
	"mono":     `ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace`,
}

Typefaces are the font stacks the binary ships. Each names a fallback, because a stack of one is a stack that fails on the machine without that font.

Functions

func ApprovalProcessFor

func ApprovalProcessFor(it Item) string

ApprovalProcessFor names the process that decides this item, or "" when it needs none. Exported for the guard that holds the table above to the order package's own resolver, which is the only thing keeping the copy honest.

func KnownOutcome added in v0.9.0

func KnownOutcome(o string) bool

KnownOutcome reports whether o is one of the Outcome constants.

func KnownTrigger added in v0.9.0

func KnownTrigger(t string) bool

KnownTrigger reports whether t is one of the Trigger constants.

func LanguageListProblem

func LanguageListProblem(langs []string) string

LanguageListProblem reports what is wrong with a catalogue's declared language list, or the empty string when nothing is.

It names the offending tag rather than the list. A refusal reading "invalid languages" against a list of four leaves somebody guessing which one, and the guess is what put the bad tag there in the first place.

A duplicate is refused for a reason of its own and not for tidiness: every per-language box in the Console is drawn from this list, so a repeated tag draws two boxes writing one key, where the second silently wins and the first looks like it was ignored.

func Publish

func Publish(in Input) (Release, []Problem)

Publish validates a catalogue set and computes its release. A non-empty problem list means nothing was published; the returned Release is then zero.

func ValidLanguageTag

func ValidLanguageTag(s string) bool

ValidLanguageTag reports whether s is a language tag this catalogue model will store: a BCP 47 tag, narrowed to the shape a maintainer types (ADR-0413).

Why this exists at all

Because a language tag is an opaque map key everywhere else in this tree. Texts are `map[string]string`, the Console draws one box per declared language, and the portal looks up `texts[locale]` — none of those can tell a tag from a sentence, and none of them fails loudly when the key is nonsense. They simply find nothing and fall back, which looks like a product that was never translated.

That is not hypothetical. A live catalogue was saved with the single language `de; en`, because the list is read comma-separated and somebody typed the separator they had been taught for a different field. One box appeared, labelled `de; en`; both names went into it; and the portal's language switch did nothing at all, for every product in that catalogue, with no screen anywhere saying why. The cost of the missing check was measured in weeks, and the check is a regexp.

The shape

A primary subtag of two or three letters, then any number of subtags of one to eight letters or digits, separated by single hyphens: `de`, `de-CH`, `zh-Hans`, `pt-BR`. Case is accepted as written, because a tag is compared as stored and re-casing somebody's list would move the key their texts are already under.

Deliberately narrower than BCP 47, which also admits grandfathered and private forms. A tag this refuses and the standard allows is a tag no catalogue in this product has ever carried, and the cost of that narrowness is one refusal a person can read. The cost of being liberal is the paragraph above.

Types

type Action added in v0.9.0

type Action struct {
	// Key is the contract: what the portal, the order and a later requirement or
	// capability name. Lower-case letters, digits and dashes; unique per product.
	Key string `json:"key"`
	// Message is the message the lifecycle process starts or waits at for this
	// action — the process's business, so renaming it changes the mapping and not
	// what an order asks for. Unique per product.
	Message string `json:"message"`
	// Effect is what the action does to the position; one of the Effect constants.
	Effect string `json:"effect"`
	// Triggers says who may ask for it; Trigger constants. The order starts the
	// provision action, so it names none.
	Triggers []string `json:"triggers,omitempty"`
	// Labels is what a button says, per language. A missing one is a reported
	// translation gap, never a refusal (ADR-0414).
	Labels map[string]string `json:"labels,omitempty"`
	// Form names an Atlas form for what the action needs — the new size, the reason
	// (ADR-0358). Like a product's configuration form, it is not resolved here.
	Form string `json:"form,omitempty"`
	// Outcomes maps an outcome to the event type it is published under. An outcome
	// it does not name is published as <message>.<outcome> (ADR-0429 §3).
	Outcomes map[string]string `json:"outcomes,omitempty"`
}

Action is one thing that can be asked of a position of this product.

func CopyActions added in v0.9.0

func CopyActions(in []Action) []Action

CopyActions copies a list of actions, so a release does not share their slices and maps with the row it was frozen from, nor an order line with its release.

func (Action) AskedOfHeld added in v0.9.0

func (a Action) AskedOfHeld() bool

AskedOfHeld reports whether the action is one somebody asks of a held position — a change or a service (ADR-0429 §2). The provision and the return are the order's own acts and have routes of their own.

func (Action) TriggeredBy added in v0.9.0

func (a Action) TriggeredBy(t string) bool

TriggeredBy reports whether t is one of the action's triggers.

type Approval

type Approval struct {
	Kind ApprovalKind `json:"kind"`
	// Ref names the principal for KindFixed or the group for KindRole. It is empty
	// for the kinds that need no target, and Publish refuses a rule that needs one
	// and has none.
	Ref string `json:"ref,omitempty"`
}

Approval is one item's approval rule.

type ApprovalKind

type ApprovalKind string

ApprovalKind names how an order line for an item gets approved.

The four kinds are configuration a catalogue maintainer picks from a list; each is carried out by a process, so a fifth is modelled and registered under a name rather than built into the product. That is why this is a string and not an enum closed by the type system: KindNone, KindFixed, KindRole and KindSuperior are the shipped four, and a registered process name is as valid a value.

const (
	// KindNone orders without approval. A catalogue always answers which of its
	// items carry this, as a standing list — see ADR-0315.
	KindNone ApprovalKind = "none"
	// KindFixed routes to one named principal.
	KindFixed ApprovalKind = "fixed"
	// KindRole routes to whoever holds a named group; any one of them may approve.
	KindRole ApprovalKind = "role"
	// KindSuperior routes to the orderer's line manager, as the orderer's account
	// records it (ADR-0470).
	KindSuperior ApprovalKind = "superior"
)

type ApproverLookup

type ApproverLookup interface {
	// Account reports whether this names an account that can hold a task. Matched
	// the way an assignee is matched: by username, ignoring case.
	Account(username string) bool
	// Group reports whether this names a group, by id or by name, ignoring case —
	// which is the order and the tolerance candidate groups are matched with.
	Group(ref string) bool
}

ApproverLookup answers whether an approval reference resolves to anybody. It is two questions rather than one because the two are matched differently, and the difference is the whole reason this report exists.

type ApproverProblem

type ApproverProblem struct {
	ItemID string `json:"itemId"`
	// HomeCatalog is where the product is maintained, because that is where the
	// reader has to go to correct it.
	HomeCatalog string `json:"homeCatalog"`
	Kind        string `json:"kind"`
	// Ref is what the rule names, echoed exactly. A reference that differs from
	// what its author believes they typed is the ordinary case here, so the report
	// shows the stored string rather than a tidied one.
	Ref string `json:"ref"`
	// Why is written for the person who has to fix it, and names the thing that
	// does not exist rather than the rule that is invalid.
	Why string `json:"why"`
}

ApproverProblem is one product whose approval rule reaches nobody.

type ApproverReport

type ApproverReport struct {
	// Checked is how many products were examined, so an empty Problems list can be
	// read as "none of 240" rather than as "nothing was looked at".
	Checked  int               `json:"checked"`
	Problems []ApproverProblem `json:"problems"`
}

ApproverReport is what one read answers.

type Baselines added in v0.17.0

type Baselines interface {
	ImportBaselines() (map[string]string, error)
	RecordImportBaselines(hashes map[string]string) error
}

Baselines remembers, per artifact, the hash of the version the last import of a document brought. Both methods run on the run loop.

type Binding added in v0.8.0

type Binding struct {
	Process string `json:"process"`
	Message string `json:"message,omitempty"`
}

Binding is where one operation of a product starts: a process, and — for a lifecycle process — the message start event in it. Message is empty for the two- process form, which starts its process by hand.

func (Binding) Bound added in v0.8.0

func (b Binding) Bound() bool

Bound reports whether the binding names anything to start.

func (Binding) Triggered added in v0.8.0

func (b Binding) Triggered() bool

Triggered reports whether this binding starts its process through a trigger rather than by hand.

type Catalog

type Catalog struct {
	ID    string            `json:"id"`
	Texts map[string]string `json:"texts"`
	// Rank resolves the one-catalogue-per-user rule against directory group
	// membership, which is many-to-many: the highest rank a user reaches wins.
	// Ranks are unique across catalogues, and Publish refuses a tie rather than
	// falling back to an id nobody chose.
	Rank int `json:"rank"`
	// Languages are the language tags this catalogue offers. A release proves every
	// item it carries has a text for each, and a companion record forbids declaring
	// one the portal interface does not have.
	Languages []string `json:"languages"`
	// Items are the item ids this catalogue offers, whatever their home.
	Items []string `json:"items"`
	// Groups are the directory or Atlas groups whose members reach this catalogue.
	Groups []string `json:"groups,omitempty"`
	// Members are the object axis (ADR-0071/0180/0278): the role says a caller may
	// maintain catalogues at all, this says which ones. Without it a product
	// manager can rebuild and publish every customer's catalogue, with nothing in
	// the way but not knowing an id — which ADR-0278 states plainly is not an
	// access control.
	Members []Member `json:"members,omitempty"`
	// Theme is this catalogue's appearance, empty for the instance's own. Set by
	// an administrator rather than by whoever maintains the catalogue
	// (decision 12).
	Theme Theme `json:"theme,omitempty"`
	// Edges are the structure and precedence between the items this catalogue
	// offers. They belong to the catalogue rather than to the items because they
	// are what its release is computed from, and because the same two products can
	// relate differently in two catalogues.
	Edges []Edge `json:"edges,omitempty"`
	// OwnerID is whoever created it, because creation is the one moment where
	// there is nobody to ask. The owner may always read, write and share.
	OwnerID string `json:"ownerId,omitempty"`
	// Revision is optimistic concurrency, the same field and the same rule
	// [Item.Revision] and the capability map carry.
	//
	// A catalogue's patch is partial, so it cannot clear a field nobody mentioned
	// the way a product's full replace could. What it can still lose is a list:
	// Items, Edges and Members are replaced whole when sent, and the surfaces that
	// send them compute the new value from a snapshot — adding one product posts
	// every product plus that one. Two maintainers adding a product a second apart,
	// and the second write is the first one's disappearance, with no error and no
	// trace.
	//
	// Zero in a patch means the caller stated no precondition and the write is
	// unconditional, which is right for a form whose every field is on the screen
	// in front of somebody. The caller holding a snapshot is the one that should
	// say so. A catalogue itself is never at zero: it is created at one, so there
	// is always a revision to hold on to.
	//
	// Every writer advances it, and that is not a detail — a path that changes a
	// catalogue without advancing it is a path whose changes a stale caller
	// overwrites in silence. TestEveryWriterAdvancesTheRevision names them all.
	Revision  int64 `json:"revision"`
	CreatedAt int64 `json:"createdAt"`
	UpdatedAt int64 `json:"updatedAt"`
}

Catalog is a named set of items, with the appearance, audience and precedence that decide who sees it and how.

func Highest

func Highest(catalogs []Catalog) (Catalog, bool)

Highest is the top-ranked catalogue of a set, and whether there was one.

It is what "which catalogue" means when there is no audience question to ask — see Service.HandleMyCatalog for the one mode where that is the case. Rank is the tie-break the product already uses everywhere else, and publishing refuses a rank tie, which is what makes "highest" an answer rather than a coin toss.

func Resolve

func Resolve(catalogs []Catalog, groups []string) (Catalog, bool)

Resolve returns the catalogue somebody in these groups sees: the highest-ranked one they reach. The second return is false when they reach none, which is an answer and not an error — it has to be distinguishable from reaching an empty catalogue.

func StripProducts added in v0.18.0

func StripProducts(c Catalog, gone map[string]bool) (Catalog, []string)

StripProducts returns c without the products in gone: they leave its offer and every edge that names one of them, and the ids it lost are answered sorted.

It is the one change an administrator's purge (ADR-0469) makes to a catalogue it does not delete. A product is referenced by catalogues rather than owned by one, so the catalogue being purged can be the home of a product another catalogue offers; deleting the product and leaving the other offer would publish a reference to nothing the next time somebody publishes there.

The caller advances the revision and the time when it writes the result. This function only says what the catalogue would look like: the plan the purge shows before it acts is computed with it too.

func (Catalog) ReachedBy

func (c Catalog) ReachedBy(groups []string) bool

ReachedBy reports whether somebody in these groups is in this catalogue's audience.

A catalogue with no audience reaches nobody. That is fail-closed on purpose: a freshly created catalogue has no groups yet, and the dangerous default is the one where it is visible to everybody while somebody is still filling it.

type CatchPoint added in v0.8.0

type CatchPoint struct {
	Element    string
	Message    string
	Correlated bool
}

CatchPoint is one element of a process that waits for a message.

type ChildRef added in v0.21.0

type ChildRef struct {
	Ref      string   `json:"ref"`
	Version  int      `json:"version"`
	Title    string   `json:"title,omitempty"`
	Passages []string `json:"passages,omitempty"`
}

ChildRef is one child page whose text a description holds.

type ContentSource added in v0.21.0

type ContentSource struct {
	// Source is the configured source's id.
	Source string `json:"source"`
	// Primary is the language whose page names the pictures.
	Primary string `json:"primary"`
	// Pages are the bound pages by language tag.
	Pages map[string]PageRef `json:"pages"`
}

ContentSource is a product's binding to its pages in an external source (§ 4, § 5 of the record).

func (*ContentSource) Copy added in v0.21.0

func (c *ContentSource) Copy() *ContentSource

Copy is a binding sharing no memory with this one, for a release.

func (*ContentSource) Oldest added in v0.21.0

func (c *ContentSource) Oldest() int64

Oldest is the earliest moment any page of the binding was copied: what a lifted quarantine compares against (contentstate.go).

type ContentSources added in v0.21.0

type ContentSources interface {
	Sources() ([]contentsource.Source, error)
	Open(id string) (contentsource.Source, contentsource.Adapter, error)
}

ContentSources is how the catalogue reaches the configured sources. Off the run loop only: each method visits it itself.

type ContentState added in v0.21.0

type ContentState struct {
	ItemID string `json:"itemId"`
	Source string `json:"source,omitempty"`
	// LastChecked is when the recheck last ran for it, and LastPassed when it
	// last passed, in the server's Unix nanoseconds.
	LastChecked int64 `json:"lastChecked,omitempty"`
	LastPassed  int64 `json:"lastPassed,omitempty"`
	// Quarantine is set from the first failed recheck until a refresh passes.
	// A recheck that passes again on its own does not lift it: the content
	// Atlas holds is the content that was in question.
	Quarantine *Quarantine `json:"quarantine,omitempty"`
	// Distrusted is the start of the latest quarantine a refresh lifted. Content
	// copied before it is the content that was in question, and it stays
	// withheld wherever it still is — in the releases published before the
	// refresh — while the refreshed copy, taken after, is shown.
	Distrusted int64 `json:"distrusted,omitempty"`
	// Own is what the product carried before it was first bound: the product
	// manager's own description, keywords and picture, which the bound content
	// replaced. An unlink puts it back (§ 6, amended), so unbinding a product
	// returns it to how it was rather than leaving it described by pages that no
	// longer supply it. Nil for a binding made before this was kept, which an
	// unlink then treats as before: the last copy stays.
	//
	// Kept here and not on the item, because it is no part of what the shop is
	// offered: a release never carries it, and a quarantine never withholds it.
	Own *OwnContent `json:"own,omitempty"`
}

ContentState is one bound product's recheck record.

type Document added in v0.9.0

type Document struct {
	Catalogs []Catalog `json:"catalogs"`
	Products []Item    `json:"products"`
	// Publish asks for a release of every catalogue in the document once it is
	// written, validated before anything is.
	Publish bool `json:"publish,omitempty"`
	// Edges are the structure and precedence between the document's products when
	// it carries products alone (ADR-0451). Edges belong to a
	// catalogue, so these land in the catalogue the products are imported into
	// (?into=), and a document carrying them is refused without one.
	Edges []Edge `json:"edges,omitempty"`
	// Processes are the BPMN models the products are bound to, deployed by the
	// import before the catalogues are written (ADR-0451).
	Processes []Process `json:"processes,omitempty"`
	// MissingProcesses names the processes an export was asked to carry and could
	// not, because the exporting server has none deployed under that id. An import
	// reads nothing from it; it is there for the person holding the file.
	MissingProcesses []string `json:"missingProcesses,omitempty"`
}

Document is the importable shape. IDs are the document's own and stable, so importing the same document again updates what the first import created rather than creating it twice.

func (Document) Check added in v0.9.0

func (d Document) Check() []DocumentProblem

Check reports what is wrong with a document on its own, without asking the store: ids, languages, duplicates, products without a home, and the fields a document may not set. It is what an example's test runs, and the first thing an import does.

type DocumentProblem added in v0.9.0

type DocumentProblem struct {
	// Subject is "catalog:<id>" or "product:<id>", or "document" for the whole.
	Subject string `json:"subject"`
	Problem string `json:"problem"`
}

DocumentProblem is one reason a document is refused, naming what it is about.

type DocumentResult added in v0.9.0

type DocumentResult struct {
	// DryRun says nothing was written: the answer is the plan.
	DryRun  bool     `json:"dryRun,omitempty"`
	Created []string `json:"created"`
	Updated []string `json:"updated"`
	// Artifacts is every catalogue, product and process of the document, with how
	// it compares to this server's and what the import did with it, or would.
	Artifacts []importplan.Artifact `json:"artifacts,omitempty"`
	Releases  []Release             `json:"releases,omitempty"`
	// Warnings are what the releases leave exposed (AnswerWarnings), by product.
	Warnings []DocumentProblem `json:"warnings,omitempty"`
	// Deployed names the document's processes this import deployed, and Unchanged
	// those whose newest deployed version already was the document's model.
	Deployed  []string `json:"deployed,omitempty"`
	Unchanged []string `json:"unchanged,omitempty"`
}

DocumentResult is what an import answers: which catalogues and products it created and which it updated, and the releases it published.

type Edge

type Edge struct {
	From string   `json:"from"`
	To   string   `json:"to"`
	Kind EdgeKind `json:"kind"`
}

Edge links two items. For the structural kinds From is the whole and To the part; for EdgeRequires From is the dependent item and To its precondition.

type EdgeKind

type EdgeKind string

EdgeKind distinguishes the two questions an edge can answer. They are different questions and were conflated in the record this implements: structure says what belongs to what, precedence says what must exist first.

const (
	// EdgeComposition is structure: the part is integral to the whole, always
	// ordered with it, never deselectable.
	EdgeComposition EdgeKind = "composition"
	// EdgeAggregation is structure: the part is optional, separately orderable, and
	// may belong to several wholes.
	EdgeAggregation EdgeKind = "aggregation"
	// EdgeRequires is precedence: From cannot be provisioned before To is. This is
	// the edge the fulfilment order is computed over, and the only one whose
	// direction means "after".
	EdgeRequires EdgeKind = "requires"
	// EdgeExcludes is incompatibility: From and To must never be held by the same
	// person (ADR-0342). The clerk who may create a supplier
	// must not also approve payments to it — neither right is wrong, the
	// combination is.
	//
	// A third question rather than a shading of the other two, and put beside them
	// for the reason they are apart: an edge kind that means two things is one
	// nobody can read.
	//
	// **The only symmetric kind.** The other three mean something different read
	// backwards; "A must not be held with B" is exactly "B must not be held with
	// A". [Publish] is where that is resolved, by writing both directions into the
	// release — a reader that checked one would find half the violations and report
	// the estate as half clean.
	EdgeExcludes EdgeKind = "excludes"
)

func (EdgeKind) Structural

func (k EdgeKind) Structural() bool

Structural reports whether this edge describes containment rather than order.

type EntryPointLookup added in v0.8.0

type EntryPointLookup interface {
	// EntryPoints returns the message names of the process's root message start
	// events, whether it has a none start, and whether any version is deployed.
	EntryPoints(processID string) (messages []string, hasNone bool, deployed bool)
}

EntryPointLookup answers what the catalogue cannot know alone: which start events the newest deployed version of a process has. It is the server's to answer, like ProcessLookup.

type ExportChoice added in v0.18.0

type ExportChoice struct {
	ID    string            `json:"id"`
	Texts map[string]string `json:"texts"`
	Bound bool              `json:"bound"`
}

ExportChoice is a catalogue a person may export, as the application package's download offers it: its id and texts, and whether a product it maintains or offers runs on one of the processes asked about.

type FormField added in v0.9.0

type FormField struct {
	Key string
	// NotPersonal is the form author's word that this answer names nobody: the
	// custom property personal=false on the field. It quiets
	// [AnswerWarnings] for this field; nothing else reads it.
	NotPersonal bool
}

FormField is one field a form asks: the variable it writes — the root of its key — and whether the form says its answer is not personal data.

type FormFieldLookup added in v0.9.0

type FormFieldLookup interface {
	FormFields(formID string) (fields []FormField, found bool)
}

FormFieldLookup answers which fields a form has. found is false for a form the server does not hold.

type FulfilmentProblem

type FulfilmentProblem struct {
	// ItemID and HomeCatalog are empty for a problem on the shared path, which
	// belongs to no single product and stops every order alike.
	ItemID      string `json:"itemId,omitempty"`
	HomeCatalog string `json:"homeCatalog,omitempty"`
	// Stage says where on the path this sits: "order", "approval", "provision" or
	// "deprovision". It is what tells a reader whether nothing can be ordered at
	// all or only this one service cannot be given back.
	Stage string `json:"stage"`
	// ProcessID is the process the binding names, echoed exactly.
	ProcessID string `json:"processId"`
	// JobType is set where the process is deployed and one of its job types is
	// unserved. Empty where the process itself is missing.
	JobType string `json:"jobType,omitempty"`
	// Why is written for the person who has to fix it and names the thing that is
	// absent, not the binding that is invalid.
	Why string `json:"why"`
}

FulfilmentProblem is one thing that would stop an order, named where the reader has to go to fix it.

type FulfilmentReport

type FulfilmentReport struct {
	// Checked is how many offered services were examined, so an empty Problems
	// list reads as "none of 10" rather than as "nothing was looked at".
	Checked int `json:"checked"`
	// Releases names the releases the products came from, so a reader can tell
	// whether the report is about what they just published.
	Releases []string            `json:"releases"`
	Problems []FulfilmentProblem `json:"problems"`
	// Remainder is, for each product converted to a lifecycle process, how many
	// order lines still start one of the processes it bound before (ADR-0427) — the
	// count that says when those processes may be retired. Empty where nothing was
	// converted or nothing is left.
	Remainder []Remainder `json:"remainder"`
}

FulfilmentReport is what one read answers.

type Import

type Import struct {
	Items []Item `json:"items"`
	Edges []Edge `json:"edges"`
	// Skipped names what was left out and why, in the model's own words where it
	// has them.
	Skipped []string `json:"skipped"`
}

Import is what a model yielded: the drafts, the structure between them, and everything that did not come across.

func ImportArchiMate

func ImportArchiMate(data []byte, homeCatalog string) (Import, error)

ImportArchiMate reads an ArchiMate Open Exchange document and derives catalogue drafts from it. The returned records are not stored; a caller compares them with what it has and decides.

type Input

type Input struct {
	// CatalogID names the catalogue this publish is *of*. Everything computed
	// below — the schedule, the structure, the standing lists — is that one
	// catalogue's, and [Items] and [Edges] are its own.
	//
	// The other catalogues are here for one question, and only one: a rank is
	// unique across the set, so a tie can only be seen against everybody else. It
	// was easy to miss that the rest of the set is not the subject, and missing it
	// cost a real defect — every other catalogue's products were resolved against
	// *this* catalogue's item list, so every one of them came back "unknown item",
	// and a catalogue could be published only while every other catalogue was
	// empty. Publishing A blamed B, publishing B blamed A, and neither message
	// named the catalogue the caller had asked to publish.
	//
	// Empty means "there is no other catalogue to be", which is true of a single
	// one and of nothing else — see [Publish], which refuses the ambiguous case
	// rather than picking a reading.
	CatalogID string
	Catalogs  []Catalog
	Items     []Item
	Edges     []Edge
}

Input is the whole of what a publish is computed from. It is passed rather than read so the computation is a pure function of its argument: the same input produces the same release and the same problems, which is what lets a release be diffed and a refusal be reproduced.

type Item

type Item struct {
	ID string `json:"id"`
	// HomeCatalog is the catalogue whose scope governs editing this item. An item
	// is referenced by catalogues rather than owned by one — it legitimately appears
	// in several — so scope cannot be inherited from "the catalogue it is in", and a
	// per-item member list is the per-artifact ACL ADR-0071 refused. One home, and
	// every other catalogue includes it read-only.
	HomeCatalog string `json:"homeCatalog"`
	State       State  `json:"state"`
	// Texts holds the item's name per language tag. A release proves it carries one
	// for every language its catalogue declares.
	Texts map[string]string `json:"texts"`
	// Descriptions is what this product *is*, per language tag, beside the name
	// Texts carries. Empty is the ordinary state for a product whose name says
	// enough, and means no description anywhere rather than an empty one.
	//
	// # Why per language and not one string
	//
	// Every other text a person reads off a product is per locale, and this is the
	// one somebody reads when the name was not enough — which is exactly when not
	// being able to read it matters. A catalogue declaring German and French could
	// otherwise translate the name of a thing and not the sentence explaining it.
	//
	// It is deliberately not Keywords, which is flat for the opposite reason:
	// keywords are for *finding* and a searcher's language is not the catalogue's,
	// while a description is for *showing* and is read in the language the portal
	// is being read in.
	//
	// # What a release demands of it
	//
	// Nothing, until there is one. A product with no description publishes, because
	// not every product needs a paragraph. A product with a description in one of
	// the catalogue's declared languages and not another does not, because that is
	// a portal where one audience is told what the thing is and the other is shown
	// an empty panel — the half-translated catalogue the name check already
	// refuses, in the field a reader turns to second.
	Descriptions map[string]string `json:"descriptions,omitempty"`
	// Rich is the description as its content source lays it out, per language
	// tag — headings, lists, tables, panels, images, embedded items — beside the
	// plain Descriptions a search and a reader without it use (ADR-0491, amendment
	// of 2026-10-28). Only a link or a refresh writes it; a save never does.
	Rich      map[string][]contentsource.Rich `json:"rich,omitempty"`
	Lifecycle Lifecycle                       `json:"lifecycle,omitempty"`
	Variants  []Variant                       `json:"variants,omitempty"`
	Approval  Approval                        `json:"approval"`
	// ProvisionProcess and DeprovisionProcess are the BPMN process ids bound to this
	// item. Both are required to publish: a catalogue that can only grant is not a
	// lifecycle, and the day somebody must revoke at scale is the wrong day to find
	// the process was never written.
	ProvisionProcess   string `json:"provisionProcess"`
	DeprovisionProcess string `json:"deprovisionProcess"`
	// LifecycleProcess binds ONE process for the whole of this item's lifecycle, in
	// place of the two ids above (ADR-0425). Each operation is a message start event
	// of it, named in Operations: provision and deprovision are required, change is
	// optional. An item carries either the two ids or this, never both.
	//
	// The operation is the contract and the message name the process's own
	// business, the way a published interface maps an entry-point name to an
	// element (ADR-0373): renaming a start event changes the mapping, not what an
	// order asks for.
	LifecycleProcess string            `json:"lifecycleProcess,omitempty"`
	Operations       map[string]string `json:"operations,omitempty"`
	// LifecycleForm says how the lifecycle process runs: "per-operation" (the
	// default, and what empty means) starts an instance for every operation;
	// "per-position" starts one instance per order position at provisioning and
	// delivers every later operation to it
	// (ADR-0428). Declared
	// rather than read off the diagram, so a catch event somebody adds to a model
	// cannot silently change how a product is returned.
	LifecycleForm string `json:"lifecycleForm,omitempty"`
	// Actions is the product's open list of actions, in place of Operations
	// (ADR-0429): every thing that can be asked of a position, each with an
	// effect the order interprets, the triggers that may ask for it, and the
	// message the lifecycle process starts or waits at. An item carries either
	// Operations or Actions, never both; one that still carries Operations is
	// read as the actions they mean (ActionList), so nothing is migrated.
	Actions []Action `json:"actions,omitempty"`
	// CommandedBy names, by their portable keys (ADR-0134), the process applications
	// whose processes may issue this product's actions with a shop command task
	// (ADR-0429 §10, decision 1) — an HR leaver process returning a right, a
	// maintenance process resetting a password. Only an action an operator or a
	// system may ask for can be issued so. Empty, which is the default, means no
	// process may command it.
	//
	// It is read from the newest release at the moment a task commands, not frozen
	// on the order: it says which automation the operator trusts with what somebody
	// holds, and taking an application off the list must stop it for every right
	// already held, not only for the ones ordered afterwards.
	CommandedBy []string `json:"commandedBy,omitempty"`
	// MultipleAllowed says whether a principal may hold this item more than once —
	// two licences, two mailboxes. Where it is false the basket marks an item the
	// orderer already holds as held, and skips it.
	MultipleAllowed bool `json:"multipleAllowed,omitempty"`
	// Targets is what this item is called in the systems that actually hold it, and
	// it is what lets a commissioning load attribute a right it found to this item
	// (ADR-0333). Empty is the ordinary state for an
	// item nothing outside Atlas grants, and it means no load will ever name it.
	//
	// Several are allowed: one service is legitimately two groups. The same ref
	// twice is not, and [Publish] refuses it — an observation matching two items
	// cannot be attributed, and guessing between them would write evidence Atlas
	// invented.
	Targets []TargetRef `json:"targets,omitempty"`
	// MaxDays is how long a right this product grants may last, in days, or zero
	// for one that does not end (ADR-0344). It is a
	// **ceiling declared as policy** — nobody holds this for more than ninety days
	// — and not a date somebody chose; an order cannot yet name a shorter end
	// within it.
	//
	// It reaches a grant through the release, like the bindings and the approval
	// rule beside it: a ceiling relaxed in the catalogue next week must not lengthen
	// a right granted this week under the stricter one.
	//
	// It never applies to an adopted or legacy right. A commissioning load records
	// a found right's start as the moment it was found, so a ceiling measured from
	// it would schedule an entire estate to expire on the anniversary of the day
	// somebody switched the portal on.
	MaxDays int `json:"maxDays,omitempty"`
	// Eligible narrows the catalogue's audience for this one product: the group ids
	// whose members may **receive** it. Empty is the ordinary case and means no
	// narrowing (ADR-0347).
	//
	// # Why an item needs this when a catalogue already has an audience
	//
	// Because a person sees exactly one catalogue — the highest-ranked one their
	// groups reach — so "put it in a narrower catalogue" is not a workaround. A
	// second catalogue does not give anybody a second shop; it is simply hidden
	// behind the one they already reach. A product that should be available to part
	// of a catalogue's audience could not be expressed at all.
	//
	// # Why empty is not fail-open
	//
	// [Catalog.ReachedBy] is fail-closed and stays the gate: nothing here is
	// reachable by somebody outside the catalogue's audience, whatever this says.
	// This only ever *narrows* that, so an empty list inherits a restriction rather
	// than removing one — the opposite of a catalogue with no groups, which would
	// otherwise be a shop open to everybody while somebody is still filling it.
	//
	// # Who is checked
	//
	// The **recipient**, never the orderer. A manager ordering a workplace for a
	// new hire is the ordinary case and must keep working; the question this
	// answers is who may end up holding the thing, and that is the person it is
	// for. It reaches an order through the release like the ceiling and the
	// approval rule beside it, so a restriction relaxed next week cannot retroact
	// on an order placed this week and a restriction *added* next week cannot
	// invalidate one already approved.
	Eligible []string `json:"eligible,omitempty"`
	// Keywords are the words somebody might search for that are not the product's
	// name: synonyms, the vendor's own term, the abbreviation everybody uses, the
	// thing it replaced (ADR-0355).
	//
	// # Why this exists rather than searching the name alone
	//
	// The story it serves names the case exactly: find a service *when the exact
	// product name is not known*. A search over names answers only for somebody who
	// already knows what the thing is called, which is the person who least needs
	// a search.
	//
	// # Why one flat list and not one per language
	//
	// Every other text on an item is per locale, and this deliberately is not. A
	// synonym list is for **finding**, not for displaying, and a searcher's language
	// is not the catalogue's: somebody reading a German catalogue types "laptop" as
	// readily as "Notebook", and an abbreviation like "M365" belongs to no language
	// at all. Splitting the list by locale would hide a term from exactly the person
	// who needed it, and would ask a product manager to maintain in two places what
	// they think of once.
	//
	// It travels into the release like everything else here, so a term added next
	// week does not change what an order placed this week was placed against — which
	// matters less for a search than for a rule, and is still the property that
	// makes a release a release.
	Keywords []string `json:"keywords,omitempty"`
	// ConfigForm names the Atlas form somebody fills in when they order this
	// product: the cost centre, the site, the employee number — whatever this one
	// product needs that its name does not say
	// (ADR-0358). Empty is the
	// ordinary case and means the product is fully described by what it is.
	//
	// # Why a form id and not a field list of its own
	//
	// Atlas already has forms: a definition with an id, an editor, a renderer, a
	// generator, and two surfaces rendering them. A second way to declare "these
	// are the fields somebody fills in" would be a second thing to author, a second
	// thing to render, and a second set of field types to keep level with the
	// first. The catalogue names an id and interprets nothing.
	//
	// # What the catalogue does *not* do with it
	//
	// It never resolves it. The form store is the `api` package's and this package
	// cannot see it, exactly as it cannot see which processes are deployed — and
	// for the same reason the two provisioning bindings are stored as plain ids.
	// The authoring screen offers only forms that exist, which is where that check
	// belongs: at the moment somebody chooses, not at the moment somebody orders.
	//
	// # Why the release freezes the id and not the form
	//
	// A release freezes what an order was placed against, and the reason is always
	// the same: a rule relaxed next week must not change what somebody was held to
	// this week. That argument is about *rules*. A form is a set of questions, and
	// what has to survive is the **answers** — which the order line carries, with
	// their field keys, for as long as the order exists. "Cost centre 4711" stays
	// true whatever the form does afterwards.
	//
	// The honest cost is stated in the record: a form that gains a field next week
	// leaves every earlier order without a value for it, and a provisioning process
	// that reads it finds nothing. That is true of every variable a process reads
	// and is the process's business; copying a form-js document into every release
	// would put a rendering artifact inside a design-time model that has kept
	// rendering out of itself on purpose, and would send it to every browser that
	// opens the portal.
	ConfigForm string `json:"configForm,omitempty"`
	// Price is what this product costs, written as the catalogue's maintainer wants
	// it read — "CHF 1'200.–", "49.– / Monat", "im Grundpaket enthalten"
	// (ADR-0361). Empty is the ordinary case
	// and means the catalogue says nothing about cost.
	//
	// # Why a string and not a number with a currency
	//
	// Because it is **displayed and never computed**. A number invites a total, a
	// total invites two products in different currencies, and that invites a rate
	// and a date — a money model, decided by an installation's finance rules and not
	// by this package. Everything that makes a price *arithmetic* is absent on
	// purpose, and a string is the honest shape of "this is what it says on the
	// shelf".
	//
	// The cost of that is stated rather than hidden: nothing can add these up. The
	// surface that needs a figure most is the approval, and an approval decides one
	// line — so the one number it shows is the one number it needs. A basket total
	// would need the money model above, and that is a different measure.
	//
	// # Why it is frozen into the release like a rule
	//
	// It is not a rule, and it travels like one anyway. An approver saw a figure and
	// decided on it; a catalogue edit next week must not make the record show a
	// different figure than the one that was approved. That is the same sentence as
	// the approval rule's and the ceiling's, and it is the reason this is on the
	// line as well as in the release.
	Price string `json:"price,omitempty"`
	// Category is the heading this product sits under in the portal — "Arbeitsplatz",
	// "Kommunikation", "Fachanwendungen" (ADR-0360).
	// Empty is the ordinary case and puts the product under the heading the portal
	// names for products that have none.
	//
	// # Why a field and not an entity
	//
	// Because it is **a heading and nothing else**. An entity would carry its own
	// texts, its own ordering, its own visibility and its own lifecycle, and each of
	// those is a thing to publish, migrate and get wrong. A heading has none of them:
	// it is the word above a column.
	//
	// The costs are real and stated rather than hidden. A category has **no ordering
	// of its own** — there is nothing on a string to sort by, and inventing a rank
	// here would be the entity arriving through the back door, so the portal sorts
	// alphabetically. And two spellings are two categories, with nothing to notice
	// that "Arbeitsplatz" and "Arbeitsplätze" were meant as one.
	//
	// If either of those turns out to matter, the answer is the entity, and this
	// field is what it would be migrated from.
	//
	// # What this string is, now that the heading is translatable
	// (ADR-0412)
	//
	// The third cost ADR-0360 recorded was that a heading has no translation. It has
	// one now, in [Item.CategoryTexts] below — and this field did not become it. It
	// is **the key**: what the portal groups by, what a search hit sets to open the
	// cascade at the right column, and what an already published release already
	// holds. The translations are how it is *rendered*.
	//
	// That split is the whole of the decision, and it is what makes the change
	// additive. Had the string become a map, two products would be one category when
	// their German agreed and two when their French did not — a heading that splits
	// in a language nobody publishing it reads. Grouping by one value that no reader
	// ever has to see keeps "same category" a fact about the catalogue rather than
	// about the reader.
	//
	// Where translations are present the key never has to be a word: a stable slug
	// survives the maintainer deciding the German wording was wrong. Where they are
	// absent — every product written before the field existed, every single-language
	// catalogue — the key is the word, and it is what every language renders. That
	// is the old behaviour exactly, which is why no stored record had to be migrated.
	Category string `json:"category,omitempty"`
	// CategoryTexts is [Item.Category] per language tag: the heading as the reader
	// of this portal reads it, where the key above is what everything groups by.
	//
	// Empty is the ordinary state and means the key renders in every language — the
	// state of every product written before this field and of every catalogue that
	// declares one language.
	//
	// # What a release demands of it
	//
	// The rule [Item.Descriptions] follows, for the same reason: optional as a
	// whole, all-or-nothing once there is one. A heading translated into German and
	// not French is a portal where one audience reads its own column head and the
	// other reads somebody else's, and nothing says so, because the fallback renders
	// and looks deliberate.
	//
	// Beside that, a translation without a key is refused. It would put the product
	// in the bucket for products carrying no heading — under a column head reading
	// "Ohne Kategorie", while holding the word for one in four languages.
	CategoryTexts map[string]string `json:"categoryTexts,omitempty"`
	// ProductGroup is the group this product sits in, one level below its category:
	// the portal's cascade reads Kategorie > Produktgruppe > Produkt > Services.
	//
	// A string on the product, exactly like [Item.Category] above, and for the same
	// reasons — see the paragraph there, which applies word for word. What is worth
	// adding is what the pairing means, because "a chain" sounds like more than this
	// is: the group has no record and therefore **no category of its own**. The
	// product carries both strings and the chain is assembled per product — this
	// product is in category X, group Y — so the group→category relation is only
	// ever read off the products that carry both.
	//
	// Two consequences, and neither is an error state. A group whose products sit
	// in two categories appears under both, because nothing anywhere claims a group
	// belongs to one. And a group with no products does not exist, exactly as a
	// category with none does not.
	ProductGroup string `json:"productGroup,omitempty"`
	// ProductGroupTexts is [Item.ProductGroup] per language tag, exactly as
	// [Item.CategoryTexts] is the heading's — same split into a key that groups and
	// texts that render, same all-or-nothing rule at publication, same reason. See
	// the paragraphs there.
	ProductGroupTexts map[string]string `json:"productGroupTexts,omitempty"`
	// ContentSource binds this product's description, keywords and picture to
	// one page per language of an external source
	// (ADR-0491). Nil — the ordinary
	// state, and the only one on an installation with no source configured — means
	// the product manager writes them, exactly as before.
	//
	// While it is set, the fields it supplies are the source's: a save may not
	// change them, and only the link, refresh and unlink routes write it. It is
	// frozen into a release like every other field, so a release says which pages
	// its content was copied from and when.
	ContentSource *ContentSource `json:"contentSource,omitempty"`
	// Revision is optimistic concurrency, the same field and the same rule the
	// capability map uses: a write that states a revision is refused when the
	// stored record has moved past it, rather than silently overwriting somebody
	// else's edit (ADR-0376).
	//
	// # Why this and not the timestamp beside it
	//
	// Because a caller has to be able to send it back unchanged, and
	// [Item.UpdatedAt] is Unix nanoseconds — around 1.8e18, past the 2^53 where a
	// float64 stops representing integers exactly. Every JSON client that decodes
	// numbers as doubles, which is most of them and every MCP client, would return
	// a value a few hundred nanoseconds off and be told its write was stale. A
	// revision counts in ones, so it stays exact for more writes than a catalogue
	// will ever see.
	//
	// Zero means the caller stated no precondition and the write is unconditional,
	// which is what the Console sends: it renders every field and posts every
	// field, so the record it overwrites is the record on the screen in front of
	// somebody. A product stored before this field existed also reads zero, so its
	// first write is unconditional and every write after it can be guarded.
	Revision  int64 `json:"revision"`
	CreatedAt int64 `json:"createdAt"`
	UpdatedAt int64 `json:"updatedAt"`
}

Item is a product or a service. Which of the two it is follows from its position in the graph, not from a field: an item nothing composes is a product, an item with no parts is a service, and an item with both is a bundle inside a bundle. Levels nest arbitrarily.

func (Item) ActionList added in v0.9.0

func (it Item) ActionList() []Action

ActionList is the product's actions, in either shape it may say them in: the declared list, or — for a product that still carries ADR-0425's operation map — the actions that map means. Nil for a product with no lifecycle process. Every consumer asks this rather than reading either field, so the two shapes cannot be read two ways.

func (Item) ActionNamed added in v0.9.0

func (it Item) ActionNamed(key string) (Action, bool)

ActionNamed is the action this product declares under key, in either shape.

func (Item) BindingFor added in v0.8.0

func (it Item) BindingFor(op string) Binding

BindingFor is where an operation of this item starts, in whichever form the item binds. The two-process form offers provision and deprovision. On a lifecycle process, provision and deprovision are the actions of those effects and any other name is an action's key (ADR-0429), read from either shape the product says its actions in.

func (Item) OwnsMessage added in v0.9.0

func (it Item) OwnsMessage(message string) (string, bool)

OwnsMessage reports whether one of this product's actions starts or waits at the message: the name no inbound watch may claim and no name-correlated publish may reach around the order (ADR-0425 §8, ADR-0429 §1). It returns the action's key.

func (Item) PerPosition added in v0.8.0

func (it Item) PerPosition() bool

PerPosition reports whether the item's lifecycle runs as one instance per order position.

func (Item) UsesLifecycleProcess added in v0.8.0

func (it Item) UsesLifecycleProcess() bool

UsesLifecycleProcess reports whether the item binds one process for its lifecycle.

type Lifecycle

type Lifecycle struct {
	From  int64 `json:"from,omitempty"`
	Until int64 `json:"until,omitempty"`
}

Lifecycle is the window in which an item may be ordered, in the server's own Unix time (nanoseconds, as every other timestamp in this tree).

Zero means unbounded on that side, which is the ordinary case: most products are orderable from the moment they are published until somebody withdraws them. The window is data rather than a computed state so that a catalogue can be published ahead of the date it opens.

**Both ends are inclusive**, and placing an order outside them is refused (ADR-0397). The gate is the order service, read from the release like every other rule about a basket; the portal keeps the same window so a basket cannot be filled with something the placement will refuse, which is courtesy rather than enforcement. The Console authors the window as two days and stores the **end** of the last one, so a product orderable "until the 31st" is orderable on the 31st.

It governs ordering and nothing else. A right already held when the window closes keeps running; when a right ends is Item.MaxDays, which is a different question (ADR-0344).

type Member

type Member struct {
	Ref  PrincipalRef `json:"ref"`
	Role MemberRole   `json:"role"`
}

Member is one grant on a catalogue.

type MemberRole

type MemberRole string

MemberRole is what a member may do. The two values are ADR-0071's, unchanged: a viewer reads, an editor reads and writes. Ownership is the implicit third and highest, and is not listed.

const (
	// RoleViewer reads the catalogue.
	RoleViewer MemberRole = "viewer"
	// RoleEditor reads and changes it, publishing releases included.
	RoleEditor MemberRole = "editor"
)

type OwnContent added in v0.21.0

type OwnContent struct {
	Descriptions map[string]string `json:"descriptions,omitempty"`
	Keywords     []string          `json:"keywords,omitempty"`
	// Picture says whether a picture was set aside beside the product's picture
	// (the store's own-picture file), to be put back on unlink.
	Picture bool `json:"picture,omitempty"`
}

OwnContent is a product's own content set aside while a source supplies it.

type PageRef added in v0.21.0

type PageRef struct {
	// Ref is the page's stable id — never what was typed. A key changes when a
	// page moves between spaces; an id does not.
	Ref string `json:"ref"`
	// Version is the page's version when it was copied, and FetchedAt when, in
	// the server's Unix nanoseconds.
	Version   int    `json:"version"`
	FetchedAt int64  `json:"fetchedAt"`
	URL       string `json:"url,omitempty"`
	Title     string `json:"title,omitempty"`
	// Passages are the protected passages removed from the copy, so a recheck
	// can tell a passage protected after the copy was taken.
	Passages []string `json:"passages,omitempty"`
	// Labels are the page's labels the keywords were taken from, after the
	// source's exclusions — what lets an unlinked slot take its keywords with it
	// without asking the source.
	Labels []string `json:"labels,omitempty"`
	// Children are the child pages whose text the description holds, depth
	// first in sibling order; they are rechecked like the page.
	Children []ChildRef `json:"children,omitempty"`
	// Embeds are the items embedded anywhere in the description and taken;
	// they are rechecked like the page.
	Embeds []ChildRef `json:"embeds,omitempty"`
}

PageRef is one bound language page and what was copied from it.

type PersonalLookup added in v0.9.0

type PersonalLookup interface {
	PersonalVariables(processID string) (names []string, deployed bool)
}

PersonalLookup answers which variables the newest deployed version of a process declares personal data (ADR-0314), and whether any version is deployed.

type PrincipalRef

type PrincipalRef struct {
	Type string `json:"type"`
	ID   string `json:"id"`
}

PrincipalRef names who a grant is for. Type is "user" or "group"; a group grant reaches everyone in it, resolved from the membership the principal already carries, so no store read happens inside an authorization check (ADR-0180).

type Problem

type Problem struct {
	// Catalog and Item name where the problem is, either or both empty when it
	// belongs to the whole input (a cycle, a rank tie).
	Catalog string `json:"catalog,omitempty"`
	Item    string `json:"item,omitempty"`
	Message string `json:"message"`
}

Problem is one reason a catalogue cannot be published. Publish returns every problem it finds rather than the first: an author who fixes one refusal per attempt is an author who eventually works around the gate.

func AnswerWarnings added in v0.9.0

func AnswerWarnings(items []Item, forms FormFieldLookup, processes PersonalLookup) []Problem

AnswerWarnings names, per product, the answers that would reach one of its processes in the clear: an answer of its configuration form that a process the product binds receives (ADR-0441) and does not declare personal data (ADR-0314). Such an answer is written to that process's history as it is, and outlives the person it names in every copy of the log.

It warns and does not refuse. Whether an answer is personal data is the form author's to say, and most are not — a cost centre, a size. A field that names nobody is marked so in the form, with the custom property personal=false, and is not warned about again; a warning that could never be answered would teach people to read past it. A process that is not deployed is not asked: publishing refuses what a lifecycle binding needs, and a process deployed later is asked at the next publish.

func LifecycleProblems added in v0.8.0

func LifecycleProblems(items []Item, look EntryPointLookup) []Problem

LifecycleProblems checks the lifecycle bindings of items against what is deployed: the process exists, every bound operation is one of its message start events, and it has no none start — an entry the catalogue never uses and a create by hand would seed (ADR-0426). It runs outside Publish, which is pure, and before a release is saved.

func OrderFormProblems added in v0.9.0

func OrderFormProblems(items []Item, forms FormFieldLookup) []Problem

OrderFormProblems refuses a product whose configuration form names a field after one of OrderVariables. A form the lookup does not hold is not a problem here: publishing has never asked whether a configuration form exists, and this check does not start to.

func TranslationGaps

func TranslationGaps(in Input) []Problem

TranslationGaps is every place a catalogue is written in one of its declared languages and not another (ADR-0414).

It is the half of a rule that used to be a refusal. Publishing demanded a name for every declared language, and a description and a heading wording in all of them once there was one in any, on the argument that a shop showing one audience a product and the other an empty row is worse than no catalogue at all. The argument was sound and the premise was not: the shop never shows an empty row, it falls back to whatever language the catalogue does have, because a name in the wrong language is better than no name. So the refusal was not protecting a reader — it was holding a usable catalogue back until the last translation arrived, which meant the first language's readers waited on the second language's translator.

A publish answers two questions and they are not the same question. **Can this be ordered against**, which is what a refusal is for and what Publish still decides; and **is this as good as it should be**, which is a thing to be told rather than blocked by. This is the second one, and it exists because removing a gate and putting nothing in its place is how a half-translated catalogue becomes invisible again — the failure the gate was built against.

What counts as a gap

A field is a gap where the product says it in at least one language and not in this one — the name, the description, the two headings, and the name of every shape it is ordered in. A product nobody described has no description gap, a product carrying no heading has no heading gap, and a heading carrying its key and no wordings has none either — the key renders in every language, which is what carrying no wording means, and which is every product written before the wordings existed. Reporting those would name the whole installed base on the first screen somebody opens.

A product named in no language at all is not reported here. It is refused by Publish, because there is nothing to fall back to.

Why per catalogue and per language

The languages are the *catalogue's*, and one product is offered by several catalogues declaring different ones (ADR-0315). The same missing French is a gap in the catalogue that declares French and no statement at all about the one that does not, so a gap names both the catalogue and the item, and a maintainer reading the list can open the thing it is about.

Pure, like Publish and for the same reason: the same input answers the same gaps, so a report can be reproduced from what it was computed over.

func (Problem) String

func (p Problem) String() string

type Process added in v0.11.0

type Process struct {
	ID  string `json:"id"`
	XML string `json:"xml"`
}

Process is one BPMN model a document carries: the process id a product binds, and the XML the newest version of it was deployed from.

type ProcessDeployer added in v0.11.0

type ProcessDeployer interface {
	// CheckProcesses reports, without deploying anything, every process the
	// caller may not deploy and every one that would not deploy. A refusal for
	// want of the role is ProblemMayNotDeploy.
	CheckProcesses(r *http.Request, procs []Process) []DocumentProblem
	// DeployProcesses deploys every process whose newest deployed version is not
	// already the document's model, through the same door a single deploy takes,
	// and names which it deployed and which it left unchanged.
	DeployProcesses(r *http.Request, procs []Process) (deployed, unchanged []string, problems []DocumentProblem, err error)
}

ProcessDeployer deploys a document's processes as the person importing it. It belongs to the server for ProcessSource's reason; nil refuses a document that carries processes.

type ProcessLookup

type ProcessLookup interface {
	// JobTypesOf returns the job types the tasks of this process id carry, and
	// whether any executable definition carries that id at all. The newest
	// deployed version answers, because that is the one an instance starts on.
	JobTypesOf(processID string) (jobTypes []string, deployed bool)
	// Served reports whether anything works that job type: the engine itself, a
	// worker seen pulling it, or — for a user task — a person.
	Served(jobType string) bool
}

ProcessLookup answers what the engine knows about the processes a catalogue names. Two questions, because a binding fails in two different ways and the difference is what the reader has to act on: a process that was never deployed is a name to correct, and a deployed process with a job type nobody pulls is a worker to start.

type ProcessSource added in v0.11.0

type ProcessSource interface {
	ProcessXML(processID string) (xml []byte, deployed bool)
}

ProcessSource answers the BPMN the newest deployed version of a process id was deployed from. It belongs to the server, which owns the deployments; nil refuses an export asked to carry processes.

type Quarantine added in v0.21.0

type Quarantine struct {
	Since  int64  `json:"since"`
	Reason string `json:"reason"`
}

Quarantine is why and since when a product's source content is withheld.

type Release

type Release struct {
	// ID, CatalogID and CreatedAt identify the release. They are assigned by
	// whoever publishes it; [Publish] computes the rest.
	ID        string `json:"id,omitempty"`
	CatalogID string `json:"catalogId,omitempty"`
	CreatedAt int64  `json:"createdAt,omitempty"`
	// Items are the item definitions as they stood when this was published, sorted
	// by id, and they are copies rather than references.
	//
	// This is the whole of the snapshot rule. An order names one release; if the
	// release pointed at rows that keep changing, an approval pending for a week
	// could be approving something else by the time it is granted. Only value data
	// is copied, so an edit to the catalogue afterwards cannot reach in here.
	Items []Item `json:"items,omitempty"`
	// Waves is every item in the input, grouped into rounds: nothing in a wave
	// depends on anything in the same wave, and everything it does depend on is in
	// an earlier one. An order names one release and follows this schedule; it
	// never recomputes it.
	//
	// A flat sequence was the first shape and could not carry the failure
	// behaviour the shop requires: a failing line must stop only the lines that
	// depend on it, and a list has already discarded the reason each item sits
	// where it does. Waves keep it — a failure stops its own successors, and the
	// rest of its wave and every independent branch continue.
	Waves [][]string `json:"waves"`
	// Requires holds each item's direct preconditions, sorted, with no entry for an
	// item that has none.
	//
	// Waves are the right unit to *run* in and the wrong unit to *start on*. At a
	// wave boundary all that is known is "the previous wave is done", which cannot
	// distinguish a line whose precondition failed from one whose precondition is
	// in the same wave and succeeded — so a wave-wide barrier either starts a line
	// whose precondition is missing, or holds one whose preconditions are all
	// there. Both are wrong, so the edges travel with the schedule and [Blocked] is
	// what the fulfilment process asks when a line fails.
	Requires map[string][]string `json:"requires,omitempty"`
	// Excludes is what must never be held together with what
	// (ADR-0342), sorted, with no entry for an item that
	// excludes nothing.
	//
	// **Every pair appears under both ids.** EdgeExcludes is the only symmetric
	// edge kind, and a release recording one direction would make each reader
	// responsible for knowing which — a reader that got it wrong would find half
	// the violations and report the estate as half clean.
	Excludes map[string][]string `json:"excludes,omitempty"`
	// Includes and Options are the structure: what a product is made of, and what
	// is offered alongside it. Both are direct edges, sorted, with no entry for a
	// product made of nothing.
	//
	// They are kept apart because they mean opposite things to a basket. An
	// inclusion is a consequence of ordering the whole — integral, never
	// deselectable — and an option is an offer. Merging them would order a second
	// screen for everybody who orders a workplace.
	Includes map[string][]string `json:"includes,omitempty"`
	Options  map[string][]string `json:"options,omitempty"`
	// WithoutApproval names every item orderable with no approval at all, sorted.
	// It is a standing list rather than a report somebody has to think to run,
	// because the role that defines approval rules is the role that publishes them.
	WithoutApproval []string `json:"withoutApproval"`
}

Release is a frozen, published catalogue: what may be ordered, in what order it is fulfilled, and what it grants without asking anybody.

func (Release) Blocked

func (r Release) Blocked(failed ...string) []string

Blocked reports every item that cannot run because one of the given items failed, transitively, sorted and each named once.

The failed items themselves are not in the result: they have a failure of their own to report, and listing them as blocked would send somebody looking in the wrong place. An unknown id blocks nothing rather than erroring — a fulfilment process asking about a line that is not in this release has a different problem, and inventing one here would hide it.

func (Release) Expand

func (r Release) Expand(selected []string) []string

Expand resolves a selection into everything it actually orders: the chosen products, plus every product any of them is made of, all the way down.

Options are not pulled in — an aggregation is an offer, and including it automatically is how somebody ends up with a second screen they never asked for. Each product appears once however many selected products contain it, which is the deduplication the same service in two bundles needs.

Anything the release does not carry is ignored rather than invented: a request naming a product that was withdrawn, or never existed, must not become a line that nothing can provision.

type Remainder added in v0.8.0

type Remainder struct {
	ItemID  string `json:"itemId"`
	Process string `json:"process"`
	Lines   int    `json:"lines"`
}

Remainder is how many lines of one product still start one old process.

type RemainderLookup added in v0.8.0

type RemainderLookup interface {
	Remainder(items []Item) ([]Remainder, error)
}

RemainderLookup counts, over the orders, the lines of these lifecycle products still bound to a process the product no longer binds. It is the server's to answer — the orders are not the catalogue's — like ProcessLookup.

type Service

type Service struct {

	// Limits are the installation's resource budgets. New sets them to
	// [limits.Default]; the server overwrites them with its own once it has read
	// the environment.
	Limits limits.Limits

	// Approvers resolves an approval rule's reference to somebody, for the report
	// in approverreport.go. It is a settable field for the reason Limits is: the
	// accounts and the groups belong to the server, which builds this service
	// before it has them.
	//
	// Nil is a real state and not a bug — a Service built by a test that does not
	// care about approvals has none — so the report refuses rather than guessing.
	// Both guesses are available and both are worse than a refusal: every
	// reference unresolvable reports the whole catalogue as broken, and every one
	// resolvable reports a clean estate nobody checked.
	Approvers ApproverLookup

	// Processes answers what is deployed and what is worked, for the report in
	// fulfilmentreport.go. Settable for Approvers' reason and nil for Approvers'
	// reason: the deployments and the workers belong to the server, and a Service
	// built without them refuses the report rather than guessing at it.
	Processes ProcessLookup

	// EntryPoints answers which start events a deployed process has, so publishing
	// can refuse a lifecycle binding that names one it does not (ADR-0425). Nil
	// skips that check, for the reason Processes may be nil.
	EntryPoints EntryPointLookup

	// Forms answers which fields a configuration form has, so publishing can refuse
	// one that names a field after a variable the order sets itself
	// (ADR-0441). Nil skips that check.
	Forms FormFieldLookup

	// Personal answers which variables a deployed process declares personal data,
	// so publishing can warn about an answer that would reach it in the clear
	// (AnswerWarnings). Nil skips the warning.
	Personal PersonalLookup

	// Models answers the BPMN a process id was deployed from, for an export that
	// carries the processes its products are bound to (export.go). Nil refuses
	// such an export rather than leaving the processes out unannounced.
	Models ProcessSource

	// Deployer deploys the processes an imported document carries (export.go).
	// Nil refuses such a document.
	Deployer ProcessDeployer

	// Baselines remembers what the last import of a document brought, so an import
	// tells a record edited here from one the document moved on (documentplan.go).
	// Nil remembers nothing: every difference is then a question.
	Baselines Baselines

	// Remainders counts the order lines a converted product left on its old
	// processes, for the fulfilment report (ADR-0427). Nil leaves the report's
	// remainder empty.
	Remainders RemainderLookup

	// Content reaches the configured content sources
	// (ADR-0491). Nil is an
	// installation with none, and it is the ordinary state: every route of the
	// binding then answers that none is configured, and nothing else changes.
	Content ContentSources

	// BindingChanged is told the source of a binding that was linked, refreshed
	// or removed, so the recheck watches the pages it now holds — and stops
	// watching the ones nothing holds any more — at once rather than at its next
	// sweep. Nil does nothing.
	BindingChanged func(source string)
	// contains filtered or unexported fields
}

Service is the catalogue API area.

func New

func New(loop *runloop.Loop, store *Store, now func() int64, admin func(*httpapi.Principal) bool) *Service

New builds the service. Every dependency is an explicit argument (ADR-0147).

func (*Service) BoundTo added in v0.21.0

func (s *Service) BoundTo(source string) ([]string, error)

BoundTo names the products whose draft binds to a source — what removing the source would leave without a recheck. Off the loop.

func (*Service) ExportChoices added in v0.18.0

func (s *Service) ExportChoices(p *httpapi.Principal, processes []string) ([]ExportChoice, error)

ExportChoices names every catalogue p may export, sorted by id, each marked bound when one of its products is bound to one of processes (ADR-0467).

func (*Service) ExportDocument added in v0.18.0

func (s *Service) ExportDocument(p *httpapi.Principal, id string, processes bool) (Document, int, string)

ExportDocument is the catalogue as GET /api/v1/catalogs/{id}/document answers it, for whoever p is; with processes, the processes its products are bound to. code and msg are the route's refusal, and code is 0 when it answers. The application package (ADR-0467) exports through it, so a catalogue reaches a package under exactly the authority it reaches a file.

Only a maintainer exports. The document is the whole record — members, approval rules, process bindings — which is more than the shop shows the people a catalogue is for, so seeing the catalogue is not enough. A catalogue the caller cannot see reads as absent, as it does everywhere.

func (*Service) HandleApproverReport

func (s *Service) HandleApproverReport(w http.ResponseWriter, r *http.Request)

HandleApproverReport answers which of the products this caller maintains name an approver that reaches nobody.

Scoped to what the caller may edit, like the product list it reads: the report names products and the people who approve them, which is maintenance information and not a public index of who approves what.

func (*Service) HandleCatalogContentRefresh added in v0.21.0

func (s *Service) HandleCatalogContentRefresh(w http.ResponseWriter, r *http.Request)

HandleCatalogContentRefresh refreshes every bound product a catalogue offers (§ 7), each all or nothing on its own. A product homed in a catalogue the caller does not maintain is skipped and said so.

func (s *Service) HandleContentLink(w http.ResponseWriter, r *http.Request)

HandleContentLink binds a product to one page per language and writes the binding and the fetched content in one draft save (§ 5). Linking refuses a page the account cannot read, one in a space not released for the shop, one failing any rule of § 8, the same page in two slots, and a page bound to another product, naming it.

func (*Service) HandleContentPreview added in v0.21.0

func (s *Service) HandleContentPreview(w http.ResponseWriter, r *http.Request)

HandleContentPreview reads one page for one language as linking would (§ 5): its resolved id, title, state and version, the text it would supply, every passage and child page the rules would drop, and every reason they refuse. Nothing is written.

func (*Service) HandleContentRefresh added in v0.21.0

func (s *Service) HandleContentRefresh(w http.ResponseWriter, r *http.Request)

HandleContentRefresh fetches every bound page of a product again and writes the result into the draft (§ 7). All or nothing: a refresh that cannot read a page names it and the reason and leaves the product exactly as it was. It is the one act that ends a quarantine.

func (*Service) HandleContentSearch added in v0.21.0

func (s *Service) HandleContentSearch(w http.ResponseWriter, r *http.Request)

HandleContentSearch searches a source for pages to bind (§ 5): title, space, key and state of each hit.

func (*Service) HandleContentStatus added in v0.21.0

func (s *Service) HandleContentStatus(w http.ResponseWriter, r *http.Request)

HandleContentStatus answers what a product is bound to, whether it is quarantined, and which sources there are to bind it to.

func (s *Service) HandleContentUnlink(w http.ResponseWriter, r *http.Request)

HandleContentUnlink removes one language page of a product's binding, or the whole binding, with a reason the product manager must give (§ 6). The fields become editable again and keep the last fetched values — unless the product is quarantined, in which case the content in question is dropped. Every unlink is recorded in the audit trail.

Removing one slot removes what it supplied: that language's description and the keywords only its labels gave. Content no binding names any more would be content no recheck covers. The primary slot goes only with the whole binding, because the picture is its.

func (*Service) HandleCreateCatalog

func (s *Service) HandleCreateCatalog(w http.ResponseWriter, r *http.Request)

HandleCreateCatalog creates one.

func (s *Service) HandleDeleteLogo(w http.ResponseWriter, r *http.Request)

HandleDeleteLogo removes a catalogue's brand mark, so the portal falls back to the operator's. Same gate as setting one: taking a brand away is as much a change to what a customer group sees as putting one there.

func (*Service) HandleDeletePicture

func (s *Service) HandleDeletePicture(w http.ResponseWriter, r *http.Request)

HandleDeletePicture removes a product's picture. Same gate as setting one: taking the picture away changes how the product is offered as much as putting one there.

func (*Service) HandleExportCatalog added in v0.11.0

func (s *Service) HandleExportCatalog(w http.ResponseWriter, r *http.Request)

HandleExportCatalog serves GET /api/v1/catalogs/{id}/document: the catalogue as a document, ?processes=true with the processes its products are bound to.

func (*Service) HandleExportProducts added in v0.11.0

func (s *Service) HandleExportProducts(w http.ResponseWriter, r *http.Request)

HandleExportProducts serves GET /api/v1/catalog-products/document?ids=a,b: the products named, and the edges of their home catalogues that join two of them, ?processes=true with the processes they are bound to.

Each must be a product the caller maintains, which is what the product list shows them; one they do not reads as absent, as it does there.

func (*Service) HandleFulfilmentReport

func (s *Service) HandleFulfilmentReport(w http.ResponseWriter, r *http.Request)

HandleFulfilmentReport answers which of the services this caller offers cannot be fulfilled on this installation.

Scoped to the catalogues the caller may edit, like the approver report and the product list: it names processes and job types, which is maintenance information about this installation's wiring.

func (*Service) HandleGetCatalog

func (s *Service) HandleGetCatalog(w http.ResponseWriter, r *http.Request)

HandleGetCatalog reads one. A missing catalogue is 404 rather than an empty object: a reader has to be able to tell "no such catalogue" from "a catalogue with nothing in it".

func (*Service) HandleGetContentImage added in v0.21.0

func (s *Service) HandleGetContentImage(w http.ResponseWriter, r *http.Request)

HandleGetContentImage serves one image of a product's description, behind the gate the product's picture is behind: whoever may see the product, and only while its content may be shown — a quarantine or a recheck gone stale withholds it as it withholds the text.

func (s *Service) HandleGetLogo(w http.ResponseWriter, r *http.Request)

HandleGetLogo serves a catalogue's brand mark, or 404 when it has none.

Unlike the instance's mark this one is *not* public. ADR-0113's logo is shown on the sign-in screen and has to be reachable before anybody is known; a catalogue's is shown inside the portal, to the group the catalogue is for. An open endpoint here would answer "does catalogue X exist" to anybody who asked, which is the catalogue-name oracle the theme record refused to open — and it would hand a customer's mark to every other customer on the instance.

So it is the catalogue's own read right, and a catalogue somebody may not read answers 404 rather than 403: the two have to be indistinguishable, or the status code is the oracle the missing endpoint would have been.

func (*Service) HandleGetPicture

func (s *Service) HandleGetPicture(w http.ResponseWriter, r *http.Request)

HandleGetPicture serves a product's picture, or 404 when it has none.

A product nobody may see and a product with no picture are the same 404, on purpose: the two have to be indistinguishable, or the status code answers the question the gate withholds.

func (*Service) HandleImport

func (s *Service) HandleImport(w http.ResponseWriter, r *http.Request)

HandleImport reads an ArchiMate model and files what it finds into a catalogue, as drafts.

It needs the authority to change the catalogue, because that is what it does. It never overwrites a product that is already stored: the model is where a catalogue starts, not something it follows (decision b4), and a second import after somebody bound processes and activated a product must not undo that work. What it left alone it says, in the same report as what it could not take — both are things the person who ran it needs to know.

func (*Service) HandleImportDocument added in v0.9.0

func (s *Service) HandleImportDocument(w http.ResponseWriter, r *http.Request)

HandleImportDocument serves POST /api/v1/catalogs/import.

?into=<catalogue> imports a document of products alone into a catalogue that exists (ADR-0451): every product is offered there, a product new to this server is homed there, and the document's edges are added to it.

?dryRun=true answers the plan and writes nothing; ?overwrite= and ?keep= decide the catalogues, products and processes that differ from this server's. One decided neither way stops the import with 409 before anything is written.

func (*Service) HandleListCatalogs

func (s *Service) HandleListCatalogs(w http.ResponseWriter, r *http.Request)

HandleListCatalogs lists the catalogues the caller maintains, lowest rank first. It is the maintenance sight, not the portal one: being the audience for a catalogue puts nothing in this list, and GET /shop/catalog answers that question instead. A customer must not learn from a listing which other customers exist.

func (*Service) HandleListItems

func (s *Service) HandleListItems(w http.ResponseWriter, r *http.Request)

HandleListItems lists the products whose home catalogue the caller maintains. A product's rules — its approval, its process bindings — belong to whoever is responsible for it, and the portal reads what it may order from a release rather than from here.

func (*Service) HandleListReleases

func (s *Service) HandleListReleases(w http.ResponseWriter, r *http.Request)

HandleListReleases lists a catalogue's releases, newest first.

func (*Service) HandleMyCatalog

func (s *Service) HandleMyCatalog(w http.ResponseWriter, r *http.Request)

HandleMyCatalog answers the first question of every portal session: which catalogue is mine.

With enforcement on, it is the highest-ranked catalogue the caller's groups reach, and reaching none is 404 rather than an arbitrary catalogue — showing somebody a shop they are not the audience for is worse than showing them nothing.

The single-user mode

With `--auth=false` there is no principal, so there are no groups, so Catalog.ReachedBy answers false for every catalogue and the portal could never resolve one. The documented development and demo mode showed an empty page and said a catalogue had not been assigned — to somebody there is no "you" to assign one to (ADR-0377).

So when there is nobody to be, the audience question is not asked and the highest-ranked catalogue is the answer. That relaxes a rule this package is otherwise strict about — a catalogue with no audience reaches nobody, fail-closed on purpose — and the reason it is safe to relax *here* and nowhere else is that the guard protects nothing in this mode: with enforcement off every catalogue is already readable through the administration routes by anybody who can reach the port. It withholds a catalogue from a person who does not exist, at the cost of the one screen the mode is for.

It is confined to a **nil** principal. A signed-in administrator still gets their own audience's catalogue and not the top-ranked one: being allowed to read every catalogue is not the same as being the audience for one, and the portal answers the second question.

func (*Service) HandlePublish

func (s *Service) HandlePublish(w http.ResponseWriter, r *http.Request)

HandlePublish validates a catalogue and writes the release.

A refused publish writes nothing: the problems are the response, and a release is only created once every one of them is gone. Reporting them all at once is deliberate — an author who fixes one refusal per attempt is an author who eventually works around the gate.

func (*Service) HandleSaveItem

func (s *Service) HandleSaveItem(w http.ResponseWriter, r *http.Request)

HandleSaveItem writes a product, creating or replacing it.

The id comes from the caller rather than being minted here: a product is referred to by name in a model, an import and a catalogue's item list, so it is a chosen identifier and not an opaque one.

Replacing safely

The write is a full replace, which is the right shape for the form that sends every field it rendered and the wrong shape for anything that changes one field of a record it read earlier: a concurrent change is overwritten with nothing to say it existed. So a caller may state the Item.Revision it read, and the write is refused as a conflict unless the stored record still carries it (ADR-0376). It is the rule the capability map already uses, spelled the same way, so a reader who knows one knows the other.

Stating it is optional, and that is deliberate rather than a transition: the Console builds its body from form fields and knows no revision, so demanding one would be a breaking change to the surface that has no concurrency problem — a person editing a form is looking at what they are about to overwrite. The precondition is for the caller that is not a person.

func (s *Service) HandleSetLogo(w http.ResponseWriter, r *http.Request)

HandleSetLogo stores a catalogue's brand mark from the raw request body.

Administration, like the theme (decision 12) and for the same reason, with one of its own: this is the only place in the catalogue where a person supplies bytes that the server later serves back to a browser. Every check that makes that safe is brandimage's, and none of it reaches a lesser role.

The body is read before the loop is entered. Reading a request body is waiting on a network, and the single writer must not wait on anybody (invariant I3).

func (*Service) HandleSetPicture

func (s *Service) HandleSetPicture(w http.ResponseWriter, r *http.Request)

HandleSetPicture stores a product's picture from the raw request body.

The gate is the one that changes the product: whoever maintains its home catalogue. A picture is part of how the product is offered, and somebody who may not rename it may not re-illustrate it either.

The body is read before the loop is entered. Reading a request body is waiting on a network, and the single writer must not wait on anybody (invariant I3).

func (*Service) HandleSetTheme

func (s *Service) HandleSetTheme(w http.ResponseWriter, r *http.Request)

HandleSetTheme sets or clears a catalogue's appearance.

Administration, not catalogue maintenance (decision 12). That is the smaller change: the logo upload keeps the gate it has, and no new file-validation surface reaches a lesser role. The cost is that ten brands queue behind one administrator — acceptable, because a brand changes rarely and a catalogue's contents change weekly.

func (*Service) HandleTranslationGaps

func (s *Service) HandleTranslationGaps(w http.ResponseWriter, r *http.Request)

HandleTranslationGaps answers it for every catalogue the caller may edit.

Scoped to editors and not to readers, because it is a to-do list and a to-do list handed to the audience is a list of what the shop has not finished.

func (*Service) HandleUnpublished

func (s *Service) HandleUnpublished(w http.ResponseWriter, r *http.Request)

HandleUnpublished answers what publishing this catalogue would change.

Read authority and not write, the same as listing the releases: it says nothing the caller cannot already assemble from GET on the catalogue and on its releases. Requiring the authority to publish would withhold the answer from exactly the person who needs to ask somebody else to.

func (*Service) HandleUpdateCatalog

func (s *Service) HandleUpdateCatalog(w http.ResponseWriter, r *http.Request)

HandleUpdateCatalog changes what a catalogue offers.

func (*Service) MayEdit added in v0.8.0

func (s *Service) MayEdit(c Catalog, p *httpapi.Principal) bool

MayEdit reports whether p may change this catalogue and its products — the right a write on a product needs, which a viewer grant does not give.

func (*Service) MayMaintain

func (s *Service) MayMaintain(c Catalog, p *httpapi.Principal) bool

MayMaintain reports whether p may see this catalogue as somebody who maintains it, rather than as somebody it is offered to.

It is [mayRead] without the audience, and the difference is the whole reason it exists. A customer reaches a catalogue to order from it; that tells them nothing about the estate behind it, and a surface that drew the catalogue's products, their provisioning processes and the engine around them for everyone the catalogue reaches would be using this store to answer a question it never agreed to answer. Maintainers, and the people a maintainer shared it with, are who that picture is for (ADR-0211 §3).

func (*Service) MayOrderFrom

func (s *Service) MayOrderFrom(p *httpapi.Principal, catalogID string) (bool, error)

MayOrderFrom reports whether this principal may place an order against the given catalogue.

Two ways in, and they answer different questions. The audience is the ordinary one: which catalogue somebody orders from is a fact about *them* — their groups — rather than about their authority, so no role opens a customer's catalogue to somebody outside it. The second is whoever maintains the catalogue, because otherwise nobody can check what they built and the first real order is the test.

It runs on the loop, so callers must not already hold it.

func (*Service) Rechecks added in v0.21.0

func (s *Service) Rechecks() contentsource.Bound

Rechecks returns the catalogue's side of the recheck.

func (*Service) Released added in v0.18.0

func (s *Service) Released(id string) (bool, error)

Released reports whether a catalogue has a release. Only a shop whose catalogues are all published here is published by the package's import: one still being built would otherwise be refused there for what is unfinished here.

type ShapeLookup added in v0.8.0

type ShapeLookup interface {
	// CatchPoints returns the newest deployed version's message catch points: for
	// each, the message it waits for and whether it declares a correlation key.
	CatchPoints(processID string) []CatchPoint
	// WaitlessCycle returns the elements of a cycle in the newest deployed version
	// that waits for nothing outside the token, or nil.
	WaitlessCycle(processID string) []string
}

ShapeLookup answers what a per-position lifecycle process owes beyond its start events: which messages it waits for, and whether it can circle without waiting. A lookup that does not implement it cannot check a per-position binding, and LifecycleProblems says so rather than passing it.

type ShopLookup added in v0.9.0

type ShopLookup interface {
	ShopOutcomes(processID string) []ShopOutcome
}

ShopLookup answers which shop send tasks the newest deployed version of a process has and what each states (ADR-0429 §4). A lookup that implements it lets LifecycleProblems refuse a product whose process never answers one of its actions — the one check an arbitrary REST call cannot be held to, because it is not recognisable as a report.

type ShopOutcome added in v0.9.0

type ShopOutcome struct {
	Element string
	Action  string
	Outcome string
}

ShopOutcome is one shop send task: the action it answers and the ending it states.

type State

type State string

State is where an item stands in its own life.

There is no deleted state, and that is load-bearing: an order placed years ago and an entitlement still held both resolve through the item, so an item is withdrawn rather than removed. A store that can delete one can orphan evidence.

const (
	// StateDraft is an item being written. It cannot be published.
	StateDraft State = "draft"
	// StateActive is an item a release may carry.
	StateActive State = "active"
	// StateWithdrawn is an item no longer orderable, still resolvable forever.
	StateWithdrawn State = "withdrawn"
)

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store holds a catalogue's three kinds of record, and the one thing a catalogue owns that is not a record: its brand mark (see logo.go).

func NewStore

func NewStore(dir string) (*Store, error)

NewStore opens (creating if needed) the directories backing a catalogue.

func (*Store) Catalog

func (s *Store) Catalog(id string) (Catalog, bool, error)

Catalog reads one catalogue. A missing record is not an error: callers have to be able to tell "there is none" from "the store is broken".

func (*Store) Catalogs

func (s *Store) Catalogs() ([]Catalog, error)

Catalogs lists every catalogue, lowest rank first.

func (s *Store) ClearLogo(catalogID string) error

ClearLogo removes a catalogue's mark, whatever format it was in. A catalogue that had none is not an error: the caller asked for a state, and it holds.

func (*Store) ClearPicture

func (s *Store) ClearPicture(itemID string) error

ClearPicture removes an item's picture, whatever format it was in. An item that had none is not an error: the caller asked for a state, and it holds.

func (*Store) ContentImage added in v0.21.0

func (s *Store) ContentImage(itemID, imageID string) (data []byte, contentType string, ok bool, err error)

ContentImage reads one image of an item's description.

func (*Store) ContentState added in v0.21.0

func (s *Store) ContentState(itemID string) (ContentState, bool, error)

ContentState reads one product's recheck record.

func (*Store) DeleteCatalog added in v0.18.0

func (s *Store) DeleteCatalog(id string) error

DeleteCatalog removes a catalogue's record, and DeleteItem and DeleteRelease the same for a product and a release. ADR-0312 gave the catalogue no delete; these exist only for the administrator's purge (ADR-0469), which a server started with --catalogue-purge=false refuses, and which decides beforehand what else has to go with them. A record already gone is not an error, so a purge that stopped halfway can be run again.

func (*Store) DeleteItem added in v0.18.0

func (s *Store) DeleteItem(id string) error

DeleteItem removes a product's record. See Store.DeleteCatalog.

func (*Store) DeleteRelease added in v0.18.0

func (s *Store) DeleteRelease(id string) error

DeleteRelease removes a release. See Store.DeleteCatalog.

func (*Store) InputFor

func (s *Store) InputFor(catalogID string, edges []Edge) (Input, bool, error)

InputFor gathers what Publish validates for one catalogue: the catalogue itself, every item it offers, and the edges between them. Every catalogue is passed rather than only this one, because the rank-uniqueness check is about the set and not about any single record.

func (*Store) Item

func (s *Store) Item(id string) (Item, bool, error)

Item reads one item.

func (*Store) Items

func (s *Store) Items() ([]Item, error)

Items lists every item, by id.

func (s *Store) Logo(catalogID string) (data []byte, contentType string, ok bool, err error)

Logo reads a catalogue's mark and the type to serve it as. A catalogue without one is not an error — it is the ordinary case, and the caller falls back.

func (*Store) Picture

func (s *Store) Picture(itemID string) (data []byte, contentType string, ok bool, err error)

Picture reads an item's picture and the type to serve it as. An item without one is not an error — it is the ordinary case, and the caller falls back.

func (*Store) Release

func (s *Store) Release(id string) (Release, bool, error)

Release reads one release.

func (*Store) Releases added in v0.18.0

func (s *Store) Releases() ([]Release, error)

Releases lists every release of every catalogue, newest first.

func (*Store) ReleasesOf

func (s *Store) ReleasesOf(catalogID string) ([]Release, error)

ReleasesOf lists one catalogue's releases, newest first.

func (*Store) RestorePicture added in v0.21.0

func (s *Store) RestorePicture(itemID string) error

RestorePicture puts an item's own picture back in place of the one a source supplied, or leaves the item without a picture when none was set aside.

func (*Store) SaveCatalog

func (s *Store) SaveCatalog(c Catalog) error

SaveCatalog writes a catalogue, replacing any record with the same id.

func (*Store) SaveContentImages added in v0.21.0

func (s *Store) SaveContentImages(itemID string, images map[string]*contentsource.Picture) error

SaveContentImages writes the images a fetch copied. One already stored is left as it is: its name is its content.

func (*Store) SaveContentState added in v0.21.0

func (s *Store) SaveContentState(c ContentState) error

SaveContentState writes one.

func (*Store) SaveItem

func (s *Store) SaveItem(i Item) error

SaveItem writes an item, replacing any record with the same id.

func (s *Store) SaveLogo(catalogID string, data []byte, contentType string) error

SaveLogo writes a catalogue's mark durably, then removes any other format the catalogue had, so switching from PNG to SVG cannot leave a stale file that [Logo] would serve instead.

func (*Store) SavePicture

func (s *Store) SavePicture(itemID string, data []byte, contentType string) error

SavePicture writes an item's picture durably, then removes any other format it had, so replacing a PNG with a JPEG cannot leave a stale file that [Picture] would serve instead.

func (*Store) SaveRelease

func (s *Store) SaveRelease(r Release) error

SaveRelease writes a release. A release is never rewritten in practice — an order names one and its contents must not move — so this is the one call that creates it.

func (*Store) StashPicture added in v0.21.0

func (s *Store) StashPicture(itemID string) (bool, error)

StashPicture sets an item's picture aside, so a picture a source supplies can take its place and an unlink can put it back. It reports whether there was one. A picture set aside before is replaced: there is one own picture per item.

type TargetRef

type TargetRef struct {
	// System is the target system's name in the installation's own vocabulary —
	// "entra", "ad", "jira". Atlas never interprets it; it only requires that an
	// observation and the item it should match agree on the spelling.
	System string `json:"system"`
	// Ref is the right as that system reports it. Compared literally, case and all:
	// a distinguished name differing only in case is two different strings to the
	// directory that issued them, and normalising here would attribute a right to
	// an item on a similarity Atlas invented.
	Ref string `json:"ref"`
}

TargetRef says what this item looks like in one target system: the group, the licence SKU, the role, named exactly as that system names it.

It exists for the commissioning load and for the reconciliation after it (ADR-0333). A target system reports rights in its own vocabulary — "CN=VPN-Users", "ENTERPRISEPACK" — and an entitlement holds a catalogue item id. Something has to join the two, and the join has to be *data a person can read*, because the whole value of a load is a report somebody checks before it is applied. A join buried in a script produces a report that says "Alice holds VPN access" and gives the reader nothing to check it against.

It is deliberately not the same knowledge as the provisioning process, though the two overlap. The process *acts*: it is what adds the membership. This is a *claim* about the target system, and a claim is the thing reconciliation can test. An installation that leaves this empty simply has an item no load will ever attribute — which is the right default, not a degraded one.

type Theme

type Theme struct {
	// Accent is the source brand colour as #rgb or #rrggbb, lower case.
	Accent string `json:"accent,omitempty"`
	// Typeface names one of [Typefaces]. It is a choice from a list rather than a
	// URL on purpose: a web-font URL would reach a third party on every portal
	// page load, carrying the visitor's address there — an outbound dependency on
	// pages that must render when nothing else is reachable, and a data flow an
	// operator in public administration cannot accept on a customer-facing
	// surface.
	Typeface string `json:"typeface,omitempty"`
}

Theme is a catalogue's appearance. An empty field means the instance's own.

func (Theme) Valid

func (t Theme) Valid() error

Valid reports whether this theme can be stored, and says why not.

type TranslationReport

type TranslationReport struct {
	// Checked is how many products were looked at, so a quiet report can be told
	// from an empty one.
	Checked int `json:"checked"`
	// Catalogs are the ids this report covers, sorted: the ones the caller may
	// maintain. A caller who maintains none gets an empty list rather than a 403,
	// because having nothing to maintain is not a refusal.
	Catalogs []string `json:"catalogs"`
	// Gaps is every place a product says something in one declared language and
	// not in another, each naming the catalogue and the product it is about.
	Gaps []Problem `json:"gaps"`
}

TranslationReport is what a maintainer still owes their catalogues.

It exists because a gate was removed. Publishing used to refuse a product named in one declared language and not another; the refusal protected nobody — the portal falls back to the language the catalogue has — but it did make the gap impossible to ignore. Removing it without this would make a half-translated catalogue invisible again, which is the failure the gate was built against, and it would arrive as "the French portal reads oddly" months later.

Read off the catalogues as they stand and NOT off their releases, unlike its two siblings. The fulfilment report reads the newest release because only what is published can park an order; this is a list of work to do, and work to do is about what is being edited. It is computed from the same input a publish is, so what it says is exactly what the next publish would have to live with.

type Unpublished

type Unpublished struct {
	// Released reports whether a release exists, and the two fields after it
	// identify the newest one.
	Released  bool   `json:"released"`
	ReleaseID string `json:"releaseId,omitempty"`
	// ReleasedAt is when it was published, in the server's own seconds.
	ReleasedAt int64 `json:"releasedAt,omitempty"`

	// Added is offered now and absent from the release: publishing puts it in front
	// of people.
	Added []UnpublishedItem `json:"added"`
	// Removed is in the release and no longer offered: **the portal is still
	// offering it**, and publishing is what takes it away. This is the one a
	// maintainer cannot otherwise see, because it is gone from every screen they
	// have and present on the one they do not.
	Removed []UnpublishedItem `json:"removed"`
	// Changed is offered in both and edited since: the portal is showing an older
	// name, price, state or description than the catalogue holds.
	Changed []UnpublishedItem `json:"changed"`
}

Unpublished is the difference between the newest release and the catalogue as it stands.

Empty lists with Released set means the two agree — the portal is serving this catalogue as it is. Released false means nothing has ever been published, which is a different sentence entirely: the portal offers nothing from here at all, and the offering below is what the first release would carry.

func (Unpublished) Any

func (u Unpublished) Any() bool

Any reports whether publishing would change anything for the people ordering.

type UnpublishedItem

type UnpublishedItem struct {
	ID string `json:"id"`
	// Texts is the item's name per language. For a removed product it is the
	// **frozen** copy's, because that is the name on the portal right now, and
	// naming it any other way would describe something the reader cannot see.
	Texts map[string]string `json:"texts,omitempty"`
	// State is the live record's, and empty for a removed product whose record is
	// no longer readable here.
	State State `json:"state,omitempty"`
}

UnpublishedItem names one product in terms a maintainer can act on.

type Variant

type Variant struct {
	ID    string            `json:"id"`
	Texts map[string]string `json:"texts"`
}

Variant is one orderable shape of an item — a laptop's size, a licence tier.

Variants are unordered on purpose. An ordering would let the basket resolve a conflict by picking the higher one, and "higher" is meaningful for a licence tier and meaningless for Windows against Linux. The basket asks the orderer instead.

type WatchLookup added in v0.9.0

type WatchLookup interface {
	WatchedMessages() map[string]bool
}

WatchLookup answers which message names the server's inbound watches publish. A lookup that implements it lets LifecycleProblems refuse an action whose message a Worker's event already publishes: that event would drive the lifecycle around the order (ADR-0425 §8, ADR-0429 §1). The watch side refuses the same pair from its end.

Jump to

Keyboard shortcuts

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