Documentation
¶
Overview ¶
Package decisiondoc serves decision documentation (ADR-0324): a published DMN decision as a stored PDF plus the prose and rule tables it describes, its version history, and the revocable public link a reader without an account follows.
It is a deliberate sibling of api/processdoc, which does the same for a BPMN process, and the two are meant to be diffed against each other. The duplication is recorded in the ADR together with the rule for undoing it: when a third artifact kind wants a document, extract the generic half first and add the third second. Generalising now would mean either migrating a landed store's records or keeping a field whose name is a lie.
The single-writer boundary is the runloop.Loop this service holds: every read and write of the store goes through it, and there is no other way from here to shared state (I3, ADR-0002). Design-time only — nothing here reaches the event log, the processor, or recovery.
Index ¶
- Constants
- func NewID() (string, error)
- type Column
- type Decision
- type Doc
- type Rule
- type Service
- func (s *Service) HandleCreate(w http.ResponseWriter, r *http.Request)
- func (s *Service) HandleDelete(w http.ResponseWriter, r *http.Request)
- func (s *Service) HandleGet(w http.ResponseWriter, r *http.Request)
- func (s *Service) HandleGetPDF(w http.ResponseWriter, r *http.Request)
- func (s *Service) HandleList(w http.ResponseWriter, r *http.Request)
- func (s *Service) HandlePrune(w http.ResponseWriter, r *http.Request)
- func (s *Service) HandlePublic(w http.ResponseWriter, r *http.Request)
- func (s *Service) HandleShare(w http.ResponseWriter, r *http.Request)
- func (s *Service) HandleUnshare(w http.ResponseWriter, r *http.Request)
- func (s *Service) LoadVersions() error
- type Store
- func (s *Store) ByShareToken(shareToken string) (Doc, bool, error)
- func (s *Store) Delete(id string) error
- func (s *Store) ForDecision(decisionID string) ([]Doc, error)
- func (s *Store) Get(id string) (Doc, bool, error)
- func (s *Store) PDF(id string) ([]byte, error)
- func (s *Store) PruneDecision(decisionID string, keep int) ([]string, error)
- func (s *Store) Save(rec Doc, pdf []byte) error
- func (s *Store) SaveRecord(rec Doc) error
Constants ¶
const PublicPath = "/public/decision-docs/"
PublicPath is the prefix of the unauthenticated share URL. It lives in one place so the minted URL and the served route cannot drift.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Column ¶
type Column struct {
// Label is the column heading a modeller gave it, empty when unnamed.
Label string `json:"label,omitempty"`
// Expression is what an input column evaluates, or an output column's variable
// name. It is the thing a reader has to match a value against.
Expression string `json:"expression,omitempty"`
// Type is the declared typeRef, empty when the column declares none.
Type string `json:"type,omitempty"`
}
Column is one column of a decision table, input or output, as the document reproduces it: what it reads (an input's expression, an output's name) and the type it declares.
type Decision ¶
type Decision struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
// Description is the decision's <description> text.
Description string `json:"description,omitempty"`
// Inputs are the input data this decision consumes, by name and declared type.
Inputs []Column `json:"inputs,omitempty"`
// HitPolicy is the table's policy ("UNIQUE", "COLLECT" …), with its aggregation
// where it has one. Empty for a decision whose logic is not a table.
HitPolicy string `json:"hitPolicy,omitempty"`
// InputColumns and OutputColumns describe the table's shape; Rules are its rows.
InputColumns []Column `json:"inputColumns,omitempty"`
OutputColumns []Column `json:"outputColumns,omitempty"`
Rules []Rule `json:"rules,omitempty"`
// Literal is the FEEL expression of a decision whose logic is a literal
// expression rather than a table — for those, this is the whole of the logic.
Literal string `json:"literal,omitempty"`
}
Decision is one decision of the model as it was documented: the prose a reader needs about it and the logic it runs, snapshotted at publish time so a later edit to the model cannot rewrite what an already-published version says.
type Doc ¶
type Doc struct {
ID string `json:"id"`
// DecisionID is the DMN decision id the document describes. It binds to the id,
// not a deployment, because a decision is documented whether or not it is
// deployed — and a business rule is usually signed off before it runs, which is
// the case this version line exists for.
DecisionID string `json:"decisionId"`
// ModelName is the DMN <definitions name>, and ModelRef the handle the decision
// is stored under when it is in the model at all.
ModelName string `json:"modelName,omitempty"`
ModelRef string `json:"modelRef,omitempty"`
// Version is a per-decisionId counter, 1-based. It answers "which documented
// state of this decision is that?", never "which deployment" — the deployment
// version (ADR-0319) answers a different question and is deliberately separate.
Version int32 `json:"version"`
Title string `json:"title,omitempty"`
Note string `json:"note,omitempty"`
CreatedAt int64 `json:"createdAt"`
CreatedBy string `json:"createdBy,omitempty"`
PDFSize int64 `json:"pdfSize"`
// Decisions is the documented decision prose and logic, snapshotted by value.
Decisions []Decision `json:"decisions,omitempty"`
// XML is the DMN source the document was produced from, so a reader of the
// history can recover the exact model a version describes.
XML string `json:"xml,omitempty"`
// login. Empty means unshared, which is the default — the artifact leaves the
// system only when someone says so.
ShareToken string `json:"shareToken,omitempty"`
}
Doc is one published documentation version of a decision: immutable metadata describing what was documented, with the PDF stored beside it.
type Rule ¶
type Rule struct {
// Inputs holds one entry per input column, in order. An empty entry is DMN's
// "this column does not constrain this rule".
Inputs []string `json:"inputs"`
// Outputs holds one entry per output column, in order.
Outputs []string `json:"outputs"`
// Description is the rule's own annotation, which is where a modeller writes
// why a rule exists.
Description string `json:"description,omitempty"`
}
Rule is one row of a decision table: the unary tests in its input cells and the values in its output cells, in column order, plus whatever was written about it.
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, so every ceiling in this service is the one operators configured.
Limits limits.Limits
// contains filtered or unexported fields
}
Service serves the decision-documentation area. Build it with New.
func New ¶
func New(loop *runloop.Loop, store *Store, allow func(clientIP string) bool, newToken func() (string, error)) *Service
New builds the documentation service over its own store directory. allow and newToken are the collaborators the server supplies; neither may touch state the loop owns.
Unlike its process sibling it takes no deployment lookup. A decision document is a sign-off artifact and is usually published *before* the decision is deployed, so the field would be empty in the common case; ADR-0324 leaves it out until somebody asks for it.
func (*Service) HandleCreate ¶
func (s *Service) HandleCreate(w http.ResponseWriter, r *http.Request)
HandleCreate records the next documentation version of a decision. Body: the produced PDF plus the decision prose and rule tables it describes.
func (*Service) HandleDelete ¶
func (s *Service) HandleDelete(w http.ResponseWriter, r *http.Request)
HandleDelete prunes a version, taking its public URL with it. A missing version is not an error, so pruning is idempotent.
func (*Service) HandleGet ¶
func (s *Service) HandleGet(w http.ResponseWriter, r *http.Request)
HandleGet returns one version in full: its metadata, the documented decisions, and the DMN source it was produced from.
func (*Service) HandleGetPDF ¶
func (s *Service) HandleGetPDF(w http.ResponseWriter, r *http.Request)
HandleGetPDF downloads a version's document.
func (*Service) HandleList ¶
func (s *Service) HandleList(w http.ResponseWriter, r *http.Request)
HandleList returns one decision's documentation history, newest version first.
func (*Service) HandlePrune ¶
func (s *Service) HandlePrune(w http.ResponseWriter, r *http.Request)
HandlePrune applies a retention limit to a decision's documentation history: it keeps the newest `keep` versions and deletes the older ones, PDF and all. Idempotent — pruning an already-short history removes nothing.
func (*Service) HandlePublic ¶
func (s *Service) HandlePublic(w http.ResponseWriter, r *http.Request)
HandlePublic serves a shared version's PDF to a reader with no account. An unknown, malformed, or revoked token is one indistinguishable 404: the response must not reveal whether a document ever existed behind it.
func (*Service) HandleShare ¶
func (s *Service) HandleShare(w http.ResponseWriter, r *http.Request)
HandleShare mints (or returns the existing) public link for one version. Idempotent by design: re-sharing must not rotate a URL that readers already hold.
func (*Service) HandleUnshare ¶
func (s *Service) HandleUnshare(w http.ResponseWriter, r *http.Request)
HandleUnshare revokes a version's public link, killing the URL.
func (*Service) LoadVersions ¶
LoadVersions rebuilds the per-decision documentation counter from the durable records, so an export after a restart continues the sequence rather than restarting it at v1 and overwriting history. It runs before the loop serves traffic, so touching the map directly here respects the single-writer invariant.
type Store ¶
Store is a durable store for decision documentation versions: one JSON record per version id under a single directory, each beside the PDF it describes. The PDF is why it wraps the shared store rather than being one — a version is two files, and both have to go together.
Every entry point rejects an id that is not bare hex before touching the filesystem. Ids here name their own files directly, so that check is what keeps an id from escaping the directory.
func NewStore ¶
NewStore opens (creating if needed) the decision-docs directory. Versions list grouped by decision, newest version first within each, tie-broken by id so the order is deterministic.
func (*Store) ByShareToken ¶
ByShareToken finds the version a public token addresses. An empty or unsafe token never matches, so the many unshared records — which carry no token — stay private.
func (*Store) Delete ¶
Delete removes a version and its document. A missing file is not an error, so cleanup is idempotent; the PDF goes with the record, since neither is meaningful without the other. It shadows the embedded store's Delete so no caller can remove a record and leave its document orphaned.
func (*Store) ForDecision ¶
ForDecision returns one decision's documentation history, newest version first.
func (*Store) Get ¶
Get returns a version's record, or ok=false if there is none. An unsafe id is a clean miss rather than a filesystem lookup. It shadows the embedded store's Get deliberately: every caller goes through the guard.
func (*Store) PDF ¶
PDF returns a version's document bytes. An unknown or unsafe id is an error rather than an empty document, so a handler cannot serve a zero-byte PDF as if it were real.
func (*Store) PruneDecision ¶
PruneDecision enforces a retention limit on one decision's documentation history: it keeps the newest `keep` versions and deletes the rest, returning the ids it removed. Every version keeps a PDF, so an unpruned archive grows without limit.
keep is clamped at zero — a negative limit would otherwise delete history it was asked to retain. keep >= the number of versions prunes nothing. Deleting the oldest is deliberate: history is answered newest-first, and the reason to prune is that ancient versions are the ones no longer worth their bytes.
func (*Store) Save ¶
Save writes a version durably: the PDF first, then the record. The order matters — a record is the thing readers discover, so it must never point at a document that is not yet on disk (I2).
func (*Store) SaveRecord ¶
SaveRecord rewrites only the metadata sidecar, leaving the stored PDF alone. Minting or revoking a share token is such an update: the document is immutable, who may read it is not.