Diagram
100%

Drag to pan · Scroll to zoom · + / − to zoom · 0 to fit · Esc to close

Skip to content

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 ​

dart
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 ​

dart
final controlledProcedure = Entity(
  name: 'procedure',
  db: const EntityDb(tableName: 'procedures'),
  history: governed,
  facets: [
    IdentityFacet(
      fields: const [
        CodeField(),
        NameField('title'),
      ],
    ),
  ],
);

The default revision actions are:

ActionMeaning
RevisionAction.newRevisioncreate a working draft from the effective revision
RevisionAction.submitRevisionfreeze and submit the draft for review
RevisionAction.publishRevisionmake the reviewed revision effective and supersede the prior one
RevisionAction.abandonRevisionabandon the draft without changing the effective revision
RevisionAction.retireRevisionretire 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:

dart
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 is update, and retire is deactivation;
  • zero levels: the actor's own grant decides, once the request carries the section's submission capture (a signature, a reason). The action record carries an approval_policy snapshot saying no approval was required, with the operation, the count, the decoded governance and the stored layers that decided (trace, one PolicyLayerRef per 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.unresolved and the path; two layers on one rung with policy.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.procedures for the current aggregate;
  • docs.procedures_versions for immutable save snapshots;
  • docs.procedures_revisions for 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 ​

dart
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:

  1. the host, versions, and revisions tables appear in DatabaseInspection;
  2. a save increments immutable history;
  3. publish_revision changes the effective revision through the action path;
  4. an undeclared operational lifecycle edge is rejected by PostgreSQL;
  5. revision actions carry the configured reason/signature requirements.

Next: Authentication, IAM, and electronic signatures.

References:

Server Runtime.