14. Governed Revisions and Database Inspection
An operational lifecycle and a document-style revision lifecycle answer different questions:
- an entity facet lifecycle asks whether the record is active, retired, approved, or otherwise operational;
- entity versioning asks which immutable save or governed revision represents the record at a point in time.
Blueprint keeps both declarations on the entity without inventing a separate master-control object.
Entity declarations come from vyuh_blueprint; DbCompiler and DatabaseInspection come from package:vyuh_blueprint_server/vyuh_blueprint_server.dart.
Operational state and revision history describe different aspects of the same record.
Choose one history posture
const noHistory = History.none();
const saveHistory = History.versions();
const governed = History.revisions(
ownedRelationships: ['documents.attachments'],
createCapture: ActionCapture.signedStandard,
updateCapture: ActionCapture.reasonStandard,
);History is sealed: NoHistory keeps current state plus normal audit, VersionHistory records immutable entity versions, and RevisionHistory adds governed revisions over that history. Author with the History.* factories only; the subtypes are public so that backends can switch on them. Every revision starts as a draft.
Put the cohesive revision declaration on the entity
final controlledProcedure = Entity(
name: 'procedure',
db: const EntityDb(tableName: 'procedures'),
history: governed,
facets: [
IdentityFacet(
fields: const [
CodeField(),
NameField('title'),
],
),
],
);The default revision actions are:
| Action | Meaning |
|---|---|
RevisionAction.newRevision | create a working draft from the effective revision |
RevisionAction.submitRevision | freeze and submit the draft for review |
RevisionAction.publishRevision | make the reviewed revision effective and supersede the prior one |
RevisionAction.abandonRevision | abandon the draft without changing the effective revision |
RevisionAction.retireRevision | retire the effective revision, leaving the record with none |
They live under the reserved $revision facet and use the same action, authorization, audit, signature, idempotency, and record pipeline as declared facet actions.
Review is expressed by the revision actions and their controls/capture, not by a requireReview flag. A record has at most one draft or in-review revision at a time; this is a fixed rule, not an option.
Put revisions under approval
Approval is declared in one place, on the history:
const approved = History.revisions(
approval: RevisionApprovalPolicy(),
);The policy names the shared workflow.master_approval workflow. Declaring an approval puts the entity under approval (Entity.underApproval), and the approval is policy driven: how many approvals each operation needs is the tenant's stored workflow.master_approval policy, kept in the PolicyModule store. Its master_approval domain has one section per operation (create, update, activation, deactivation), each with approval_count (0–6), levels (reviewer groups and the outcomes that need remarks), decision_esign, witness, submission and exclude_editors. When a manual submit_revision or retire_revision runs, the server resolves that section inside the action, at the governed entity type and record:
- submit of a record with no effective revision is
create, any other submit isupdate, and retire isdeactivation; - zero levels: the actor's own grant decides, once the request carries the section's
submissioncapture (a signature, a reason). The action record carries anapproval_policysnapshot saying no approval was required, with the operation, the count, the decoded governance and the stored layers that decided (trace, onePolicyLayerRefper layer). A submit pins those facts on the revision (approval_admission,approval_pin), and its publish is judged by that pin: a later policy edit neither blocks it nor unlocks a revision that entered review through its approval workflow; - one or more: the manual action is refused with
action.revision_transition_invalid, with the same facts as params, and only the approval workflow moves the revision; - no layer, an unset parameter, or a layer that does not decode is refused with
policy.unresolvedand the path; two layers on one rung withpolicy.ambiguous. A missing policy is never read as zero.
The server reads the layers through the resolver the host binds (BlueprintPersistenceConfig.revisionApprovalResolver, usually masterApprovalResolver from vyuh_blueprint_workflow_server over the policySource of the configured BlueprintDatabaseAdapter), on the action's own transaction and tenant context, locking the layers it applies. Without one it refuses every manual submit, publish and retire. The client marks those actions as depending on the approval policy and leaves the decision to the server. Zero approvals means that no approval takes place, never that the submitter approves.
Under an approval, capture is policy too: no revision action, create or save declares a signature or reason (RevisionHistory.governedActions), and the section's opening (create and new revision), editing (save), submission (submit and retire), publication (publish, as pinned at submit) and abandonment decide them (action.signature_required, action.reason_required). A governed entity that declares its own revision action capture is refused at validation (blueprint.validation.approval_capture_declared). Products seed the tenant default masterApprovalDefaults(): no approvals, a reason only to drop a record (abandon and retire), and signatures only on decisions (decision_esign, locked at the tenant rung).
History.revisions() without an approval is ungoverned: no policy is read, the manual actions run on the actor's grant, and their captures are the declared RevisionHistory.defaultActions (a reason to open, submit and abandon; a signature and a reason to publish and retire). RegulatedModule.history(P) returns revisions under the default policy; a module that names another approval workflow overrides it.
Understand the generated tables
For a host table docs.procedures, the compiler emits:
docs.proceduresfor the current aggregate;docs.procedures_versionsfor immutable save snapshots;docs.procedures_revisionsfor governed revision state and lineage.
The host row carries revision identity, number, status, parent revision, and effective revision columns. The revision table preserves review/effective state without creating a second competing save-version counter.
Inspect emitted structure, not a UI guess
final compilation = DbCompiler.compile(blueprint);
final inspection = DatabaseInspection.from(
blueprint: blueprint,
compilation: compilation,
);
final json = inspection.json;
print(json['summary']);
print((json['sql'] as Map)['complete_script']);DatabaseInspection parses the emitted DDL and runtime DDL. Its manifest contains:
- schemas, entities, tables, columns, and constraints;
- indexes and relationships;
- lifecycle and infrastructure triggers;
- views and their SQL;
- the complete installation script.
Because the manifest comes from emitted SQL, the Studio database inspector and migrations describe the same physical plan.
Verify lifecycle enforcement too
Facet lifecycles lower to PostgreSQL guards. For declared edges such as draft -> approved and approved -> retired, the emitted trigger rejects any other update of the state column with a constraint error.
This gives two independent controls:
- the action runtime authorizes and records the requested transition;
- PostgreSQL rejects an undeclared state edge even if a writer bypasses the normal action UI.
Checkpoint
Create a revisioned SOP entity and prove:
- the host, versions, and revisions tables appear in
DatabaseInspection; - a save increments immutable history;
publish_revisionchanges the effective revision through the action path;- an undeclared operational lifecycle edge is rejected by PostgreSQL;
- revision actions carry the configured reason/signature requirements.
Next: Authentication, IAM, and electronic signatures.
References: