ngx-t-workflow-typings 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,6 +65,18 @@ export interface ElementEditorInnerSectionStepElementInterface {
65
65
  label: string;
66
66
  value: string;
67
67
  }>;
68
+ /**
69
+ * Renders the chip list as a MULTI-select, so the bound value is a `string[]`
70
+ * rather than one value.
71
+ *
72
+ * Already honoured at runtime — `t-dynamic-data-edit.component.html:183` binds
73
+ * `[multiple]="!!vm.editorConfigValue?.multipleSelection"` for every chip
74
+ * editor, and the workflow-level config (`ElementEditorConfig`) has always
75
+ * declared it. It was simply missing from the STEP-level declaration, so a
76
+ * step element that needed a multi-select (the document/print field lists)
77
+ * could not say so without a cast.
78
+ */
79
+ multipleSelection?: boolean;
68
80
  computedErrors?: ValidationError[];
69
81
  min?: number;
70
82
  max?: number;
@@ -22,6 +22,21 @@ export interface SimilarityResult {
22
22
  view: boolean;
23
23
  relation: 'ancestor' | 'descendant' | 'sibling' | 'similar';
24
24
  }
25
+ /**
26
+ * Document-level lifecycle state, written by the check-in / check-out flow.
27
+ *
28
+ * Distinct from {@link DocumentWorkflowStep.status}, which is a per-step
29
+ * `WorkflowStepStatus` on `processTree[]`. This one describes the document itself.
30
+ *
31
+ * Members are additive only — never remove one, since existing documents carry the
32
+ * value and Mongoose validates the whole document on `.save()`.
33
+ */
34
+ export declare enum WorkFlowDocumentStatus {
35
+ /** Live document. Set on the original when a checked-out copy is checked back in. */
36
+ Active = "active",
37
+ /** Superseded check-out copy, retained for history. Set together with `archive: true`. */
38
+ Archived = "archived"
39
+ }
25
40
  /**
26
41
  * The STORED fields of a workflow document (the `FormVal` collection), excluding
27
42
  * everything Mongoose adds at runtime (`_id`, `createdAt`, `updatedAt`).
@@ -59,6 +74,12 @@ export interface leanWorkFlowDocumentInterface {
59
74
  date: Date;
60
75
  };
61
76
  checkedOutDocId?: string;
77
+ /**
78
+ * Document lifecycle state. Optional: only documents that have been through the
79
+ * check-in / check-out flow carry it, so every document written before that
80
+ * feature — and every ordinary transaction — has no `status` at all.
81
+ */
82
+ status?: WorkFlowDocumentStatus;
62
83
  latestVersion: number;
63
84
  versions: Array<{
64
85
  versionNumber: number;
@@ -69,6 +90,56 @@ export interface leanWorkFlowDocumentInterface {
69
90
  /** UI-only: in-flight indicator. */
70
91
  busy?: boolean;
71
92
  SYSTEM_TAGS: ITransactionTag[];
93
+ /**
94
+ * Present when the document is a FINAL RECORD — see {@link DocumentSeal}.
95
+ *
96
+ * This is THE seal indicator every consumer reads. It travels with the document
97
+ * through every existing endpoint (list, detail, reports), so no surface needs a
98
+ * separate "is it sealed" call: presence is the answer, and the fields inside it
99
+ * carry who, when, and until when it can still be reversed.
100
+ */
101
+ seal?: DocumentSeal;
102
+ }
103
+ /**
104
+ * The stamp that makes a workflow document a final record: no further editing,
105
+ * check-out, archiving, restoring or deletion.
106
+ *
107
+ * Lives on the document (not only in the sealing ledger) because that is what makes
108
+ * enforcement atomic — the check-out guard tests `seal: { $exists: false }` in the
109
+ * same `$match` it already runs — and what lets every read path report the state
110
+ * without a join.
111
+ */
112
+ export interface DocumentSeal {
113
+ /** When the record was sealed. */
114
+ at: Date;
115
+ /**
116
+ * Who sealed it. Name and email are SNAPSHOT at seal time, never looked up later:
117
+ * a record has to stay readable after the person leaves or is renamed.
118
+ */
119
+ by: {
120
+ userId: string;
121
+ name?: string;
122
+ email?: string;
123
+ };
124
+ /** Why, when the sealer gave a reason. */
125
+ reason?: string;
126
+ /** Digest algorithm, recorded so a later change to it stays verifiable. */
127
+ algo: 'sha256';
128
+ /** Hex digest of the record's canonical content at the moment of sealing. */
129
+ hash: string;
130
+ /**
131
+ * Which canonical-payload recipe produced {@link hash}. Verification compares this
132
+ * BEFORE comparing hashes, so a seal written by an older recipe reports "cannot be
133
+ * checked" rather than falsely reporting the record as altered.
134
+ */
135
+ payloadVersion: number;
136
+ /**
137
+ * The instant the seal becomes permanent. Before it, the sealer or an administrator
138
+ * may reverse the seal; after it, no code path does. Stored as an absolute instant
139
+ * rather than derived from a configured window, so changing that policy cannot move
140
+ * the terms a record was sealed under.
141
+ */
142
+ hardenAt: Date;
72
143
  }
73
144
  /** A hydrated workflow document: the stored fields plus the ones Mongoose adds. */
74
145
  export interface WorkFlowDocumentInterface extends leanWorkFlowDocumentInterface {
@@ -4,3 +4,19 @@ export var RecentlyViewedStatus;
4
4
  RecentlyViewedStatus["Complete"] = "complete";
5
5
  RecentlyViewedStatus["Action"] = "action";
6
6
  })(RecentlyViewedStatus || (RecentlyViewedStatus = {}));
7
+ /**
8
+ * Document-level lifecycle state, written by the check-in / check-out flow.
9
+ *
10
+ * Distinct from {@link DocumentWorkflowStep.status}, which is a per-step
11
+ * `WorkflowStepStatus` on `processTree[]`. This one describes the document itself.
12
+ *
13
+ * Members are additive only — never remove one, since existing documents carry the
14
+ * value and Mongoose validates the whole document on `.save()`.
15
+ */
16
+ export var WorkFlowDocumentStatus;
17
+ (function (WorkFlowDocumentStatus) {
18
+ /** Live document. Set on the original when a checked-out copy is checked back in. */
19
+ WorkFlowDocumentStatus["Active"] = "active";
20
+ /** Superseded check-out copy, retained for history. Set together with `archive: true`. */
21
+ WorkFlowDocumentStatus["Archived"] = "archived";
22
+ })(WorkFlowDocumentStatus || (WorkFlowDocumentStatus = {}));
@@ -1,4 +1,4 @@
1
- export type { RecentlyViewedParams, SimilarityResult, leanWorkFlowDocumentInterface, WorkFlowDocumentInterface, IRecentlyViewed, IRecentlyViewedCookie, } from './WorkFlowDocument.interface.js';
2
- export { RecentlyViewedStatus } from './WorkFlowDocument.interface.js';
1
+ export type { RecentlyViewedParams, SimilarityResult, leanWorkFlowDocumentInterface, WorkFlowDocumentInterface, IRecentlyViewed, IRecentlyViewedCookie, DocumentSeal, } from './WorkFlowDocument.interface.js';
2
+ export { RecentlyViewedStatus, WorkFlowDocumentStatus } from './WorkFlowDocument.interface.js';
3
3
  /** @deprecated Use the {@link RecentlyViewedStatus} enum. */
4
4
  export type { RecentlyViewedStatusType } from './WorkFlowDocument.interface.js';
@@ -1 +1 @@
1
- export { RecentlyViewedStatus } from './WorkFlowDocument.interface.js';
1
+ export { RecentlyViewedStatus, WorkFlowDocumentStatus } from './WorkFlowDocument.interface.js';
@@ -121,6 +121,32 @@ export interface DocumentWorkflowStep extends ProcessStep {
121
121
  * @see LegacyDocumentWorkflowStep if a consumer still needs the opaque shape.
122
122
  */
123
123
  validations: StepValidationOverride[];
124
+ /**
125
+ * Escalation records stamped onto the step when it is written.
126
+ *
127
+ * @deprecated **DUE REVIEW — pending removal.** The workflow builder's escalation
128
+ * setup has been decommissioned, so nothing configures escalations any more and
129
+ * every write is an empty array. It is declared only because writers still emit the
130
+ * key (`osproc-be/src/services/form.ts:671, 1033, 1407`; also on that service's own
131
+ * step interface, `src/interfaces/IFormVal.ts:6`) and a typed step schema would
132
+ * otherwise drop it silently.
133
+ *
134
+ * Remove this field — and the writes — once it is confirmed no stored document
135
+ * carries a non-empty `escalations` array. Deliberately `unknown[]`, NOT the
136
+ * definition-side escalation-level shape, which is already withdrawn (see
137
+ * `WorkflowSubSchemas.schema.ts`).
138
+ */
139
+ escalations?: unknown[];
140
+ /**
141
+ * The working copy a check-in came from, stamped on the `checkin` step record.
142
+ *
143
+ * Evidence: written at
144
+ * `cartalist-client-server/src/api/repositories/TransactionRepository.ts:518`. The
145
+ * matching `versions[].tempDocId` is already declared on the FormVal schema; this is
146
+ * the same link on the step record, which is what ties a filed version to the edit
147
+ * that produced it.
148
+ */
149
+ tempDocId?: string;
124
150
  }
125
151
  export interface DocumentActivityLog extends DocumentWorkflowStep {
126
152
  /**
@@ -200,6 +200,62 @@ export interface ProcessStep extends IProcessNodeBase {
200
200
  adjudicationMicroFlowStepWithAssignPoints?: string;
201
201
  microFlow: string;
202
202
  formId?: string;
203
+ /**
204
+ * Hides this step's section from the transaction's SYSTEM DOC — the assembled
205
+ * document a transaction's whole team can read.
206
+ *
207
+ * A workflow accumulates form values step by step and the System Doc renders
208
+ * one section per step that captured any, so an internal or procedural step
209
+ * (an inbox triage, a routing capture, a step whose fields only exist to drive
210
+ * a decision gate) publishes its fields to everyone with access to the
211
+ * document. This flag is the admin's opt-out for exactly those steps.
212
+ *
213
+ * PRESENTATION ONLY, and only on that one surface:
214
+ *
215
+ * - The step still RUNS. Its form is still filled in, its values still land in
216
+ * `document.form`, and every downstream reader — decision gates, bulk mail,
217
+ * scheduling, reports — still resolves them. This is not access control and
218
+ * must not be used as such; a hidden step's values remain readable through
219
+ * the document payload.
220
+ * - The ACTIVITY LOG is unaffected. That surface is the audit trail: it must
221
+ * keep showing every visit, by whom and when, whatever the document chooses
222
+ * to publish.
223
+ *
224
+ * Owned by the workflow DEFINITION, never by a document's step record — an
225
+ * admin toggling it must change what an already-filed document renders, so
226
+ * consumers read it off the definition rather than a runtime snapshot.
227
+ *
228
+ * Absent/`false` means visible, so every workflow authored before this field
229
+ * existed keeps rendering exactly as it did.
230
+ */
231
+ hideFromDocument?: boolean;
232
+ /**
233
+ * `formControlName`s of this step's own form that are suppressed EVERYWHERE the
234
+ * document is presented — details and print alike.
235
+ *
236
+ * Use for a field the step needs but the record should not carry: an internal
237
+ * note, a routing selector, a scratch value a decision gate reads. The value is
238
+ * still captured and still reaches `document.form`; it simply is not published.
239
+ *
240
+ * @see hideFromDocument, which is the same decision one level up. A hidden STEP
241
+ * publishes nothing at all, so these lists are moot on one — precedence is
242
+ * step-hidden > field-hidden > printable, and consumers apply it in that order.
243
+ */
244
+ hiddenFields?: string[];
245
+ /**
246
+ * `formControlName`s of this step's own form that are shown on screen but left
247
+ * OUT OF PRINT.
248
+ *
249
+ * The third state of the publishing rule, and the only one that distinguishes
250
+ * the two surfaces: a field the team should be able to read in the document
251
+ * without it landing in the printed/PDF copy — working notes, a long internal
252
+ * justification, an attachment thumbnail that prints as a wall of nothing.
253
+ *
254
+ * Ignored for a field already in {@link hiddenFields} (that field is gone from
255
+ * both surfaces) and for a step with {@link hideFromDocument}. Absent means
256
+ * printable, so every field authored before this existed still prints.
257
+ */
258
+ nonPrintableFields?: string[];
203
259
  members?: string[];
204
260
  followUpOperations: BackEndFollowUpOperationInterface[];
205
261
  active?: boolean;
@@ -10,9 +10,19 @@ export declare enum WorkflowStepTypeEnum {
10
10
  PublicPortal = "publicPortal",
11
11
  Archive = "archive",
12
12
  Checkout = "checkout",
13
- Checkin = "checkin"
13
+ Checkin = "checkin",
14
+ /**
15
+ * Record sealing. Like `Checkout`/`Checkin` these are custody events rather than
16
+ * steps a document moves through — they carry no `stepId` and no definition in any
17
+ * workflow — but they ARE persisted into `processTree`, which is why they belong in
18
+ * this enum: `iserve-shared-types/schemas/Workflow/WorkflowProcessTree.schema.ts`
19
+ * derives that column's `enum` constraint from these values, so a step type absent
20
+ * here cannot legally be written.
21
+ */
22
+ Seal = "seal",
23
+ Unseal = "unseal"
14
24
  }
15
- export type WorkflowStepTypes = WorkflowStepTypeEnum.Initiate | WorkflowStepTypeEnum.Review | WorkflowStepTypeEnum.Append | WorkflowStepTypeEnum.Process | WorkflowStepTypeEnum.Decision | WorkflowStepTypeEnum.End | WorkflowStepTypeEnum.BulkMail | WorkflowStepTypeEnum.Adjudication | WorkflowStepTypeEnum.PublicPortal | WorkflowStepTypeEnum.Archive | WorkflowStepTypeEnum.Checkout | WorkflowStepTypeEnum.Checkin;
25
+ export type WorkflowStepTypes = WorkflowStepTypeEnum.Initiate | WorkflowStepTypeEnum.Review | WorkflowStepTypeEnum.Append | WorkflowStepTypeEnum.Process | WorkflowStepTypeEnum.Decision | WorkflowStepTypeEnum.End | WorkflowStepTypeEnum.BulkMail | WorkflowStepTypeEnum.Adjudication | WorkflowStepTypeEnum.PublicPortal | WorkflowStepTypeEnum.Archive | WorkflowStepTypeEnum.Checkout | WorkflowStepTypeEnum.Checkin | WorkflowStepTypeEnum.Seal | WorkflowStepTypeEnum.Unseal;
16
26
  /**
17
27
  * Placeholder step kinds the CLIENT fabricates for display. These are NEVER
18
28
  * persisted and are deliberately kept OUT of {@link WorkflowStepTypeEnum}.
@@ -12,6 +12,16 @@ export var WorkflowStepTypeEnum;
12
12
  WorkflowStepTypeEnum["Archive"] = "archive";
13
13
  WorkflowStepTypeEnum["Checkout"] = "checkout";
14
14
  WorkflowStepTypeEnum["Checkin"] = "checkin";
15
+ /**
16
+ * Record sealing. Like `Checkout`/`Checkin` these are custody events rather than
17
+ * steps a document moves through — they carry no `stepId` and no definition in any
18
+ * workflow — but they ARE persisted into `processTree`, which is why they belong in
19
+ * this enum: `iserve-shared-types/schemas/Workflow/WorkflowProcessTree.schema.ts`
20
+ * derives that column's `enum` constraint from these values, so a step type absent
21
+ * here cannot legally be written.
22
+ */
23
+ WorkflowStepTypeEnum["Seal"] = "seal";
24
+ WorkflowStepTypeEnum["Unseal"] = "unseal";
15
25
  })(WorkflowStepTypeEnum || (WorkflowStepTypeEnum = {}));
16
26
  /**
17
27
  * Placeholder step kinds the CLIENT fabricates for display. These are NEVER
@@ -650,12 +650,13 @@ export const STEP_REFERENCE_RULES = {
650
650
  label: 'Post title field',
651
651
  targetKind: ReferenceTargetKind.FormControlInMicroFlowForm,
652
652
  severity: StepRequirementSeverity.Warning,
653
- message: "Names a control that is not in the micro-flow's initiate form — the form this step actually renders. WARNING only: no read site exists in the audited repositories (the portal front end is outside them), and the editor offers the workflow-wide union rather than that form, so a mismatch may be intentional.",
653
+ message: "Names a control that is not in the micro-flow's initiate form — the form this step actually renders. WARNING only: no read site is known in any of the repositories searched, and the editor offers the workflow-wide union rather than that form, so a mismatch may be intentional.",
654
654
  evidence: [
655
655
  'W/shared/functions/hydrateStep.ts:99-108 (a public-portal node\'s form IS the micro-flow initiate form; the step has no `formId` of its own)',
656
656
  'W/component/workflow-diagram/WorkflowStepDefault.ts:102-114 (`formId` is in neither array for this type)',
657
657
  'W/component/workflow-diagram/processStepEditorConfig.ts:278-285 (picker scope is the workflow-wide union — the divergence)',
658
- 'T/Workflow/WorkflowProcessTree.schema.ts:46 (persisted; no reader in the four audited repositories)',
658
+ 'T/Workflow/WorkflowProcessTree.schema.ts:46 (persisted)',
659
+ "SEARCH NOW EXHAUSTIVE (2026-08), correcting the previous wording: the earlier grading said 'no read site in the AUDITED repositories' and named the public portal front end as the gap. That gap is closed — `iserve-portal`, `cartalist-portalApp` and the portal BACKEND (`S/services/portal/PortalService.ts`, `S/controllers/portal.ts`) contain zero reads of `publicPortalConfig`. RETAINED DELIBERATELY nonetheless, unlike the sibling `controlName` field which was retired in the same pass: the owner's call was to keep this one. The warning grading is therefore what it always was — a hint, never a block — and the 'may be intentional' clause still governs.",
659
660
  ],
660
661
  requires: [ReferenceCatalog.Workflows, ReferenceCatalog.Forms],
661
662
  requiresExternalCatalog: true,
@@ -698,22 +699,6 @@ export const STEP_REFERENCE_RULES = {
698
699
  requires: [ReferenceCatalog.WorkflowInputs],
699
700
  requiresExternalCatalog: true,
700
701
  },
701
- {
702
- id: 'publicPortal.controlName',
703
- stepType: WorkflowStepTypeEnum.PublicPortal,
704
- field: 'controlName',
705
- label: 'Step control name',
706
- targetKind: ReferenceTargetKind.FormControlInMicroFlowForm,
707
- severity: StepRequirementSeverity.Warning,
708
- message: "Names a control that is not in the micro-flow's initiate form. WARNING only: no public-portal code path in the audited repositories dereferences it.",
709
- evidence: [
710
- 'W/component/workflow-diagram/WorkflowStepDefault.ts:103 (requiredProperties)',
711
- 'W/component/workflow-diagram/processStepEditorConfig.ts:35-42 (generic control-name Input — no picker, so no scope is enforced at authoring time)',
712
- 'W/shared/functions/hydrateStep.ts:99-108',
713
- ],
714
- requires: [ReferenceCatalog.Workflows, ReferenceCatalog.Forms],
715
- requiresExternalCatalog: true,
716
- },
717
702
  {
718
703
  id: 'publicPortal.publicPortalFunctionalScoreSheetStep',
719
704
  stepType: WorkflowStepTypeEnum.PublicPortal,
@@ -762,6 +747,8 @@ export const STEP_REFERENCE_RULES = {
762
747
  [WorkflowStepTypeEnum.Archive]: [],
763
748
  [WorkflowStepTypeEnum.Checkout]: [],
764
749
  [WorkflowStepTypeEnum.Checkin]: [],
750
+ [WorkflowStepTypeEnum.Seal]: [],
751
+ [WorkflowStepTypeEnum.Unseal]: [],
765
752
  };
766
753
  /* -------------------------------------------------------------------------- */
767
754
  /* Workflow-level rules */
@@ -806,6 +793,19 @@ export const EXCLUDED_STEP_REFERENCES = [
806
793
  // `publicPortalFunctionalScoreSheetStep` target TYPE (scoreCreation), the
807
794
  // review `controlName` toggle rule, and the `postExpiryDateControlName` date
808
795
  // rule. They are no longer exclusions; see STEP_REFERENCE_RULES.
796
+ {
797
+ field: 'controlName',
798
+ stepType: WorkflowStepTypeEnum.PublicPortal,
799
+ reason: "THE FIELD NO LONGER EXISTS ON THIS TYPE. Retired 2026-08 together with its requirement entry: `WorkflowStepDefault.ts` no longer lists `controlName` in the public-portal `requiredProperties` or `properties`, so the editor offers no control to author it. A rule that can only fire on a value nothing can set and nothing can read is pure noise — and this one fired often, because the retired control was a free-text Input (no picker, no scope) while the rule resolved against the micro-flow's INITIATE form, three hops out. THE SEARCH IS NOW EXHAUSTIVE: the previous grading said 'no read site in the AUDITED repositories' and named the public portal front end as the gap. Both portal applications (`iserve-portal`, `cartalist-portalApp`) and the portal backend (`S/services/portal/PortalService.ts`, `S/controllers/portal.ts`) have since been searched and contain zero reads; `cartalist-portalApp` carries only a type declaration on its local `ProcessStep`. Every remaining reader in the estate is `stepType`-scoped to review or adjudication.",
800
+ evidence: [
801
+ 'E/services/workflow.ts:322-325 (the only routing dereference, guarded by `stepType === Review`)',
802
+ 'S/executiveDashboard/drilldown/predicates.ts:90-92 (`if (s.stepType === WorkflowStepTypeEnum.Review)` before the read)',
803
+ 'P/app-configs/ngx-t-forms-config.service.ts:561,580,598 (the adjudication reads)',
804
+ 'S/executiveDashboard/drilldown/getTransactionDrilldown.ts:57-61 (step-type-agnostic, but only widens a Mongo projection — no behaviour depends on it)',
805
+ 'W/component/workflow-diagram/WorkflowStepDefault.ts (public-portal `requiredProperties` / `properties` — `controlName` removed from both)',
806
+ "STEP_TYPE_REQUIREMENTS[publicPortal].integrityNotes (the matching removal note)",
807
+ ],
808
+ },
809
809
  {
810
810
  field: 'workflowFormForScoreSheet',
811
811
  stepType: WorkflowStepTypeEnum.Adjudication,
@@ -480,22 +480,6 @@ export const STEP_TYPE_REQUIREMENTS = {
480
480
  ],
481
481
  graphValidatorCode: 'DECISION_GATE_NO_OUTPUTS',
482
482
  },
483
- {
484
- field: 'controlName',
485
- label: 'Step control name',
486
- type: StepFieldValueType.String,
487
- severity: StepRequirementSeverity.Error,
488
- actionLabel: 'Step control name',
489
- hint: 'Set it in the "Step control name" field for the decision gate.',
490
- message: 'Give the decision gate a control name — it is required before the gate can be saved.',
491
- evidence: [
492
- 'PRODUCT OWNER POLICY (2026-07): the decision gate\'s step control name is required. This is a POLICY requirement, not an interpreter requirement — see the tension below.',
493
- 'W/component/workflow-diagram/WorkflowStepDefault.ts:149 (requiredProperties DOES list controlName — the descriptor agrees it is required).',
494
- 'TENSION — NO KNOWN READ SITE: a prior engine audit found the gate resolver dereferences `controlName` nowhere. `W/component/workflow-diagram/function/getNextWorkflowStepFromDecisionGate.ts:12-43` reads only systemStep, outputs, decisionsCondition and connections; `W/services/workflow/workflow.service.ts:70-71,137-139` delegates to that resolver with no `controlName` read. If requiring it ever produces authoring friction, that is because this is a POLICY requirement, not an interpreter one — no read site justifies it and the demotion evidence still stands.',
495
- 'W/component/workflow-diagram/processStepEditorConfig.ts:35-42 (generic control-name Input — the control that satisfies it).',
496
- ],
497
- divergesFromDescriptor: 'RESTORED to required (Error) per product owner policy (2026-07). A previous revision DEMOTED it to optional/warning on the AUDIT (c) finding that the decision-gate code path dereferences `controlName` nowhere — that finding still stands (see the NO KNOWN READ SITE note in evidence). The product owner requires it regardless; this is a policy requirement, honoured here, not an interpreter one. It re-aligns with `WorkflowStepDefault.ts:149`, which lists it in requiredProperties.',
498
- },
499
483
  ],
500
484
  conditional: [],
501
485
  optional: [],
@@ -513,7 +497,7 @@ export const STEP_TYPE_REQUIREMENTS = {
513
497
  graphValidatorCode: 'DECISION_GATE_NO_OUTPUTS',
514
498
  },
515
499
  integrityNotes: [
516
- "AUDIT (c) + PRODUCT OWNER POLICY (2026-07): `controlName` is in `WorkflowStepDefault.ts:149` requiredProperties but read by nothing in the decision path the gate resolver (`getNextWorkflowStepFromDecisionGate.ts:12-43`) and `workflow.service.ts:70-71,137-139` dereference it nowhere. A prior revision demoted it to optional/warning on that basis. The product owner now requires it, so it is a REQUIRED (Error) rule above. RECORD OF THE TENSION: this is a POLICY requirement, not an interpreter one. No read site is currently known; if requiring it ever produces authoring friction, the AUDIT (c) 'no reader' finding is the reason to revisit the policy, not the interpreter. The promotion rests solely on the owner's decision and its re-alignment with the descriptor.",
500
+ "REMOVED (2026-08), superseding the 2026-07 policy. `controlName` carries no rule here, and `WorkflowStepDefault.ts` no longer lists it in this type's `requiredProperties` or `properties`, so the editor does not offer it. HISTORY, because this field has been flipped twice: AUDIT (c) found no reader and demoted it; the product owner restored it as an explicit POLICY requirement in 2026-07, with the standing note that authoring friction would be the trigger to revisit. That friction arrived, and the owner's 2026-08 decision is to remove it rather than require a value nothing consumes. THE EVIDENCE IS NOW STRONGER THAN 'no reader found': the engine RETURNS EARLY for this step type — `E/services/workflow.ts:315-317` (`if (definitionOfLastStep?.stepType === 'decision') return { processStep: definitionOfLastStep }`) sits ABOVE the only `controlName` dereference in the routing path (`:322-325`, guarded by `stepType === Review` besides). So the field is not merely unread on this path, it is unreachable on it. The gate resolver (`getNextWorkflowStepFromDecisionGate.ts:12-43`) and `workflow.service.ts:70-71,137-139` confirm the client mirror. Every other reader in the estate is `stepType`-scoped to review or adjudication (`S/executiveDashboard/drilldown/predicates.ts:90-92`, `P/app-configs/ngx-t-forms-config.service.ts:561,580,598`). Persisted values are inert, not migrated.",
517
501
  "`members` is in `properties` (`WorkflowStepDefault.ts:151`) but the type is a system step, and `workflowDiagramActions.ts:216,289` force `members: []` on every save when `systemStep === true`. The entry is dead.",
518
502
  "Per-branch `decisionsCondition.expression` is NOT encoded as a field rule here: `getNextWorkflowStepFromDecisionGate.ts:25-27` skips an outlet without one, and the graph validator already reports it as `DECISION_GATE_MISSING_CONDITION` (`validateWorkflow.ts`, the per-output loop that pushes it when `!isNonEmptyString(expression)`). Owned by the graph validator, deliberately not duplicated. WORDING ALIGNMENT: that code emits 'has no condition set, so no document will ever be sent down it. Give it a condition or remove it.' — this type's `outputs` required-rule message and `sockets` message are worded to agree (every branch needs its own condition, or it never receives a document). The graph validator is upgrading `DECISION_GATE_MISSING_CONDITION` from warning to error; that severity change is owned there, not here.",
519
503
  ],
@@ -1059,19 +1043,6 @@ export const STEP_TYPE_REQUIREMENTS = {
1059
1043
  ],
1060
1044
  divergesFromDescriptor: 'PROMOTED to error (corrected). Previously `warning` because the only reader found took the key as a request parameter. `osproc-be` reads it directly off the step to schedule the post-expiry job, and its absence makes the portal step self-complete instantly. It agrees with `WorkflowStepDefault.ts:104` after all. NOTE the pool also changes: the engine resolves it against the MAIN document\'s form (`document.form`), not the micro-flow initiate form the node renders.',
1061
1045
  },
1062
- {
1063
- field: 'controlName',
1064
- label: 'Step control name',
1065
- type: StepFieldValueType.String,
1066
- severity: StepRequirementSeverity.Warning,
1067
- actionLabel: 'Step control name',
1068
- message: 'Optional for a public portal step — nothing in the systems checked here reads it, so you can leave it blank.',
1069
- evidence: [
1070
- 'W/component/workflow-diagram/WorkflowStepDefault.ts:103 (requiredProperties)',
1071
- 'W/component/workflow-diagram/processStepEditorConfig.ts:35-42 (generic control-name Input)',
1072
- ],
1073
- divergesFromDescriptor: 'DEMOTED to warning. AUDIT (c): required by the descriptor, dereferenced by nothing on the public-portal path.',
1074
- },
1075
1046
  ],
1076
1047
  conditional: [],
1077
1048
  optional: [
@@ -1109,7 +1080,8 @@ export const STEP_TYPE_REQUIREMENTS = {
1109
1080
  sockets: LINEAR_SOCKETS,
1110
1081
  integrityNotes: [
1111
1082
  "DESCRIPTOR/BADGE CONFLICT: `elementTemplate.systemStep` is `false` (`WorkflowStepDefault.ts:119`) while `members` and `formId` are in NEITHER `properties` nor `requiredProperties` (`:102-114`). `nodeValidator.ts:5-12` therefore permanently badges every public-portal node with 'Node is missing: user form, members' — two fields the type deliberately does not use. Either the template should be `systemStep: true` or `nodeValidator` should exempt this type; owned by the library, not fixed here. NOTE the engine DEPENDS on the empty `members`: `E/services/workflow.ts:812-813` reaches the post-expiry branch only because `members?.length > 0` is false. Making this type a system step would be safe, but giving a public-portal step members would silently disable its expiry scheduling.",
1112
- 'CORRECTED: three of the five `requiredProperties` were previously demoted to warnings for want of a reader. `postExpiryDateControlName` is now back at `error` — `osproc-be` reads it to schedule the post-expiry job (`E/services/workflow.ts:813-840`). `publicPortalConfig` and `controlName` remain at `warning`: re-checked against `osproc-be`, both still have ZERO occurrences anywhere in that repository, so the demotion holds for them.',
1083
+ 'CORRECTED: three of the five `requiredProperties` were previously demoted to warnings for want of a reader. `postExpiryDateControlName` is now back at `error` — `osproc-be` reads it to schedule the post-expiry job (`E/services/workflow.ts:813-840`). `publicPortalConfig` remains at `warning`: re-checked against `osproc-be`, it still has ZERO occurrences anywhere in that repository, so the demotion holds for it.',
1084
+ "REMOVED (2026-08): `controlName` no longer appears here at all, and `WorkflowStepDefault.ts` no longer lists it in this type's `requiredProperties` or `properties`, so the editor does not offer it. The earlier revisions could only DEMOTE it (required -> warning) because the search for a reader was bounded by four repositories and the public portal FRONT END was outside them — an unaudited consumer is a reason to keep a field, not to delete it. That gap is now closed: the two portal applications (`iserve-portal`, `cartalist-portalApp`) and the portal BACKEND (`S/services/portal/PortalService.ts`, `S/controllers/portal.ts`) were searched and contain ZERO reads of `step.controlName` — `cartalist-portalApp` carries only a type declaration on its local `ProcessStep` model. With every consumer accounted for, a field nothing reads is not a warning to be silenced, it is a field to be removed: it cannot be got wrong, so it cannot be flagged. Persisted values are left untouched by the schema (`controlName` is still a real field for `review` and `adjudication` steps) and are simply inert on this type. See also the matching `EXCLUDED_STEP_REFERENCES` entry, which retires the cross-reference rule for the same reason.",
1113
1085
  ],
1114
1086
  },
1115
1087
  /* ====================================================================== */
@@ -1202,6 +1174,38 @@ export const STEP_TYPE_REQUIREMENTS = {
1202
1174
  'Synthesised by the server on check-in (`S/repositories/TransactionRepository.ts:512-520`) and treated identically to `checkout` by the interpreter (`workflow.service.ts:57-63`) and the status resolver (`workflow-document-utils.ts:347-350`).',
1203
1175
  ],
1204
1176
  },
1177
+ /* ====================================================================== */
1178
+ /* SEAL / UNSEAL — record custody, never authored */
1179
+ /* ====================================================================== */
1180
+ [WorkflowStepTypeEnum.Seal]: {
1181
+ stepType: WorkflowStepTypeEnum.Seal,
1182
+ label: 'Record Sealed',
1183
+ origin: StepTypeOrigin.Runtime,
1184
+ systemStep: null,
1185
+ required: [],
1186
+ conditional: [],
1187
+ optional: [],
1188
+ sockets: NO_SOCKET_EXPECTATION,
1189
+ integrityNotes: [
1190
+ 'Not authorable: no palette entry, no step editor. A workflow can never contain a seal step — it is a custody event the server records when a completed document is made a final record.',
1191
+ 'Carries no `stepId`, so every "where is this document" reading must look past it; see `documentLifecycle.LIFECYCLE_EVENT_STEP_TYPES` (server) and `shared/utils/workflow-lifecycle.ts` (client).',
1192
+ 'Declared here because `processTree.stepType` derives its database `enum` constraint from this enum — a persisted step type absent from it cannot legally be written.',
1193
+ ],
1194
+ },
1195
+ [WorkflowStepTypeEnum.Unseal]: {
1196
+ stepType: WorkflowStepTypeEnum.Unseal,
1197
+ label: 'Seal Reversed',
1198
+ origin: StepTypeOrigin.Runtime,
1199
+ systemStep: null,
1200
+ required: [],
1201
+ conditional: [],
1202
+ optional: [],
1203
+ sockets: NO_SOCKET_EXPECTATION,
1204
+ integrityNotes: [
1205
+ 'Recorded only when a seal is reversed inside its grace window; past that window no code path produces this event.',
1206
+ 'Same custody-event rules as `seal` — never authored, no `stepId`, skipped by every step resolver.',
1207
+ ],
1208
+ },
1205
1209
  };
1206
1210
  /**
1207
1211
  * The 12 `WorkflowStepTypeEnum` members in a stable order, derived from the map
@@ -1,5 +1,152 @@
1
1
  import type { DocumentSectionConfigurationsInterface } from '../DocumentSection/DocumentSectionConfigurations.interface.js';
2
2
  import type { ProcessStep } from '../ProcessStep/ProcessStep.interface.js';
3
+ /**
4
+ * WHEN a transaction of a workflow may be archived, expressed as a window over
5
+ * the transaction's position in its own process tree.
6
+ *
7
+ * - `inflight-abandon` — anywhere EXCEPT a completed end step. Archiving is an
8
+ * abandon: a request that should not proceed is taken off the active list
9
+ * while it is still running. This is the behaviour every workflow had before
10
+ * the policy existed, and remains the default.
11
+ * - `on-completion` — ONLY at a completed end step. The inverse reading:
12
+ * archiving is records disposal, so finished work can be filed away and
13
+ * nothing half-done can disappear.
14
+ * - `at-step` — only while the transaction sits on one of the `stepId`s listed
15
+ * in {@link ArchivePolicy.steps}. Archiving becomes a particular step's own
16
+ * affordance ("the people on Verification can bin it there, nobody else").
17
+ * - `always` — any position.
18
+ * - `never` — archiving is not offered at all. For registers that must retain
19
+ * every transaction, this is the workflow-wide equivalent of what sealing does
20
+ * to a single record.
21
+ *
22
+ * The `auto-*` presets sketched during design (auto-archive on completion, after
23
+ * a retention window, or on stale in-flight transactions) are deliberately NOT
24
+ * here: they need a scheduled runner and a system actor for the `processTree`
25
+ * marker. Adding them later only widens this union, so it is a non-breaking
26
+ * change for stored configs.
27
+ */
28
+ export type ArchivePolicyPreset = 'inflight-abandon' | 'on-completion' | 'at-step' | 'always' | 'never';
29
+ /**
30
+ * WHO may archive, once {@link ArchivePolicyPreset} says the transaction is in
31
+ * an archivable position.
32
+ *
33
+ * - `initiator` — a member of the workflow's initiate step. The only rule that
34
+ * existed before this field, and the default.
35
+ * - `stepMembers` — the members of the step the transaction currently sits on.
36
+ * The natural pairing with the `at-step` preset; also the answer to the
37
+ * standing complaint that a step member who spots a bad transaction has to go
38
+ * find its initiator to have it removed.
39
+ * - `admin` — department or organisation administrators (inclusive upwards),
40
+ * the same right that already governs sealing.
41
+ * - `workflowMembers` — anyone named on ANY step of the workflow. The widest
42
+ * setting available, and still bounded: it is the workflow's own people, not
43
+ * "whoever can see the row". An earlier draft offered `any` for that; it was
44
+ * removed because "can see it" is a read permission and archiving is a write,
45
+ * so it granted removal to an audience nobody had chosen for the purpose.
46
+ *
47
+ * `workflowMembers` here means the same set as {@link SealActors} `workflowMembers`
48
+ * — the union of every step's members — and is NOT `stepMembers`, which is only
49
+ * the step a transaction is currently sitting on.
50
+ */
51
+ export type ArchiveActors = 'initiator' | 'stepMembers' | 'admin' | 'workflowMembers';
52
+ /**
53
+ * WHO may seal a completed transaction — and, being the same people, who may
54
+ * reverse a seal while it is still inside its grace window.
55
+ *
56
+ * - `authorOrAdmin` — the person who created the transaction, or an
57
+ * administrator of its department/organisation. The rule that predates this
58
+ * policy, and the default.
59
+ * - `author` — only the creator.
60
+ * - `admin` — administrators only. This is the separation-of-duty setting: an
61
+ * author who does not administer the department cannot finalise their own
62
+ * record. It is expressed through the actor list rather than a separate flag
63
+ * because "who may seal" is the only question being asked.
64
+ * - `initiator` — anyone on the workflow's INITIATE step.
65
+ * - `workflowMembers` — anyone named on ANY step of the workflow.
66
+ *
67
+ * AUTHOR vs INITIATOR are different scopes and must not be conflated. The author
68
+ * is `FormVal.userId` — whoever created THIS transaction. An initiator is a
69
+ * member of the workflow's initiate step: a standing permission to START
70
+ * transactions, held regardless of whether they ever touched this one. The
71
+ * author is normally also an initiator, but need not be — step membership
72
+ * changes, and the author of an old record may have left it.
73
+ *
74
+ * `workflowMembers` is also NOT the archive policy's `stepMembers`. That one
75
+ * means the members of the step a transaction is CURRENTLY sitting on; this one
76
+ * is the union of every step's members. Same-sounding, deliberately different
77
+ * sets — a sealed transaction has finished, so it sits on no actionable step.
78
+ */
79
+ export type SealActors = 'authorOrAdmin' | 'author' | 'admin' | 'initiator' | 'workflowMembers';
80
+ /**
81
+ * Per-workflow rule for sealing its transactions into final records. See
82
+ * {@link WorkflowModel.sealPolicy}.
83
+ */
84
+ export interface SealPolicy {
85
+ /**
86
+ * Whether this workflow offers sealing at all.
87
+ *
88
+ * Sealing is OPT-IN: absent or `false` means the action is not available on
89
+ * this workflow and the server refuses it. Nothing else in this object is read
90
+ * while it is off.
91
+ *
92
+ * Defaulting to `false` is also what makes the builder's Toggle safe — see the
93
+ * note on {@link ArchivePolicy.oneWay} for why a true-default boolean cannot be
94
+ * edited with one.
95
+ */
96
+ enabled: boolean;
97
+ /** Defaults to `'authorOrAdmin'` when absent. */
98
+ actors?: SealActors;
99
+ /**
100
+ * How many days a seal stays reversible.
101
+ *
102
+ * Absent means the server's `SEAL_GRACE_DAYS` environment value. `0` makes a
103
+ * seal permanent the instant it is applied.
104
+ *
105
+ * Read ONCE at seal time and stored on the seal as `hardenAt`, an absolute
106
+ * instant. Changing this never moves the terms a record was already sealed
107
+ * under — and once `hardenAt` passes, nothing and nobody can reverse that seal
108
+ * through any interface. That refusal is what the seal is worth.
109
+ */
110
+ graceDays?: number;
111
+ }
112
+ /**
113
+ * Per-workflow rule for archiving its transactions. See
114
+ * {@link WorkflowModel.archivePolicy}.
115
+ */
116
+ export interface ArchivePolicy {
117
+ preset: ArchivePolicyPreset;
118
+ /**
119
+ * The `stepId`s at which archiving is offered.
120
+ *
121
+ * Required — and only read — when `preset` is `'at-step'`; ignored under every
122
+ * other preset. Matched against the transaction's `currentStepID`, which is
123
+ * the same `ProcessStep.stepId` value the engine writes when it advances a
124
+ * document.
125
+ *
126
+ * An id that no longer appears in `processTree` (its step was deleted in the
127
+ * editor) simply never matches. The engine IGNORES unknown ids rather than
128
+ * erroring, so deleting a step degrades to "not archivable there" instead of
129
+ * breaking every transaction on the workflow.
130
+ */
131
+ steps?: string[];
132
+ /** Defaults to `'initiator'` when absent. */
133
+ actors?: ArchiveActors;
134
+ /**
135
+ * Makes archiving final: the engine refuses the restore half of the toggle.
136
+ *
137
+ * Defaults to `false` — archive and restore are a single toggle endpoint
138
+ * (`POST api/form/archiveDocument` flips `archive` whichever way it currently
139
+ * points), which is the only behaviour that exists today.
140
+ *
141
+ * Phrased as "one-way" rather than the more readable `allowRestore` because
142
+ * the workflow builder edits it with a Toggle, and every Toggle in that panel
143
+ * binds a boolean whose safe default is `false`. An `allowRestore` field would
144
+ * default to `true`, so a Toggle emitting `false` for a value the workflow has
145
+ * never carried would silently make archiving irreversible on every workflow
146
+ * whose config panel was merely opened.
147
+ */
148
+ oneWay?: boolean;
149
+ }
3
150
  /**
4
151
  * A workflow's STORED fields — everything the application writes, and nothing
5
152
  * Mongoose adds for itself (`_id`, `createdAt`, `updatedAt`, `__v`).
@@ -78,8 +225,93 @@ export interface WorkflowModel {
78
225
  *
79
226
  * Optional, so existing workflow object literals that never set it keep
80
227
  * compiling and `StrictSchemaDefinition<IDbWorkflow>` is unaffected.
228
+ *
229
+ * See {@link WorkflowModel.uniqueKeysScope} for whether ARCHIVED transactions
230
+ * still hold their tuple.
81
231
  */
82
232
  uniqueKeys?: string[];
233
+ /**
234
+ * Whether archived transactions still occupy their {@link
235
+ * WorkflowModel.uniqueKeys} tuple.
236
+ *
237
+ * - `'active'` — the duplicate check ignores archived transactions, so
238
+ * archiving RELEASES the tuple and a new transaction may re-use it. This is
239
+ * the behaviour that predates the field (the engine's duplicate query has
240
+ * `archive: false` hard-coded), and so remains the default when absent.
241
+ * - `'all'` — archived transactions still block. The tuple is claimed for the
242
+ * life of the record whatever its storage state; releasing it means
243
+ * restoring the transaction and editing the offending value, or deleting it
244
+ * outright. Note there is no "archive it out of the way" escape hatch under
245
+ * this setting — that is the point of it, but it does mean a mistaken value
246
+ * on an archived transaction needs a deliberate correction.
247
+ *
248
+ * Only consulted when `uniqueKeys` is non-empty. Enforced by
249
+ * `FormService.generateFormValues` (osproc-be) at transaction-create time,
250
+ * which includes or omits `archive: false` in the duplicate query accordingly.
251
+ *
252
+ * READ THIS TOGETHER WITH {@link WorkflowModel.archivePolicy}. Archiving is the
253
+ * only way a transaction leaves the uniqueness pool, so the two fields settle
254
+ * one question between them: whether archiving is a route around the
255
+ * constraint. A workflow that both enforces `uniqueKeys` and permits archiving
256
+ * should set this deliberately rather than inherit the default.
257
+ */
258
+ uniqueKeysScope?: 'active' | 'all';
259
+ /**
260
+ * When a transaction of this workflow may be archived, and by whom.
261
+ *
262
+ * Archiving is a soft delete: `FormVal.archive` flips to `true`, an
263
+ * `{ isArchived, date, userId }` marker is pushed onto the document's own
264
+ * `processTree`, and the row moves from the active list to the archived one
265
+ * (`getArchivedFormValByWorkflowID`). It is reversible unless
266
+ * {@link ArchivePolicy.oneWay} says otherwise.
267
+ *
268
+ * Absent => `{ preset: 'inflight-abandon', actors: 'initiator' }`, which is
269
+ * exactly what every workflow did before this field existed: the initiator may
270
+ * abandon a transaction that is still running, and the action is withdrawn
271
+ * once it reaches a completed end step. Adding the field therefore changes no
272
+ * existing workflow's behaviour.
273
+ *
274
+ * Two guards sit BELOW the policy and are not configurable by it:
275
+ * - a sealed record can never be archived or restored under any preset
276
+ * (`SealedRecordError`) — that is half of what sealing means;
277
+ * - the server re-checks the whole policy in `FormService.archiveDocument`.
278
+ * The client's `canArchive` is a UI affordance only; before this field the
279
+ * step and initiator rules lived exclusively in the Angular list projection
280
+ * and the endpoint enforced nothing but the seal.
281
+ *
282
+ * READ THIS TOGETHER WITH {@link WorkflowModel.uniqueKeysScope} — see the note
283
+ * there.
284
+ */
285
+ archivePolicy?: ArchivePolicy;
286
+ /**
287
+ * Whether this workflow's transactions can be sealed into final records, and
288
+ * on whose terms.
289
+ *
290
+ * Sealing makes a completed transaction permanent: it can never again be
291
+ * edited, checked out, archived, restored or deleted. It is reversible only
292
+ * inside a grace window recorded on the seal itself as `hardenAt`; past that
293
+ * instant NO interface and NO role can reverse it, which is the entire value of
294
+ * the seal.
295
+ *
296
+ * OPT-IN. Absent — or `enabled: false` — means this workflow does not offer
297
+ * sealing at all, and the server refuses the action. This is a deliberate
298
+ * change from the behaviour that predates the field, where every workflow could
299
+ * be sealed: finalising a record is a decision about a class of document, not a
300
+ * capability every workflow should carry by default. Records already sealed
301
+ * stay sealed, verifiable and reversible-in-window whatever this says — turning
302
+ * sealing off withdraws the ability to seal ANEW, it does not unseal anything.
303
+ *
304
+ * Eligibility is unchanged and NOT configurable: only a transaction that is
305
+ * complete, unlocked, not a working copy, not archived and not already sealed
306
+ * can be sealed. {@link SealPolicy.enabled} is a gate in front of those rules,
307
+ * never a replacement for them.
308
+ *
309
+ * READ THIS TOGETHER WITH {@link WorkflowModel.archivePolicy}. Both decide what
310
+ * may happen to a transaction once it finishes, and they interact: a sealed
311
+ * transaction can no longer be archived under ANY archive policy, because the
312
+ * seal guard refuses the write.
313
+ */
314
+ sealPolicy?: SealPolicy;
83
315
  }
84
316
  /**
85
317
  * A persisted workflow as it comes back from Mongo: the stored fields plus the
@@ -1,2 +1,2 @@
1
- export type { WorkflowModel, DbWorkflowModel } from './WorkflowModel.interface.js';
1
+ export type { WorkflowModel, DbWorkflowModel, ArchivePolicy, ArchivePolicyPreset, ArchiveActors, SealPolicy, SealActors, } from './WorkflowModel.interface.js';
2
2
  export type { WorkflowPointingRuleInterface } from './WorkflowPointingRule.interface.js';
@@ -23,6 +23,48 @@ const documentSectionConfigSchema = Joi.object({
23
23
  labelConfiguration: Joi.array().required(),
24
24
  }).optional(),
25
25
  });
26
+ /**
27
+ * Archive rule for a workflow's transactions — see the `archivePolicy` note on
28
+ * `WorkflowModel` for what each preset means.
29
+ *
30
+ * `steps` is only meaningful under the `at-step` preset, and is required there:
31
+ * an `at-step` policy with no steps would offer archiving nowhere, which is
32
+ * `never` written the confusing way. Under every other preset the key is
33
+ * forbidden rather than merely ignored, so a stale `steps` list cannot sit in a
34
+ * stored config implying a rule that is not in force.
35
+ */
36
+ const archivePolicySchema = Joi.object({
37
+ preset: Joi.string()
38
+ .valid('inflight-abandon', 'on-completion', 'at-step', 'always', 'never')
39
+ .required(),
40
+ steps: Joi.array()
41
+ .items(Joi.string())
42
+ .when('preset', {
43
+ is: 'at-step',
44
+ then: Joi.array().items(Joi.string()).min(1).required(),
45
+ otherwise: Joi.forbidden(),
46
+ }),
47
+ actors: Joi.string()
48
+ .valid('initiator', 'stepMembers', 'admin', 'workflowMembers')
49
+ .optional(),
50
+ oneWay: Joi.boolean().optional(),
51
+ });
52
+ /**
53
+ * Sealing rule for a workflow's transactions — see the `sealPolicy` note on
54
+ * `WorkflowModel`.
55
+ *
56
+ * `enabled` is required because the object exists to answer that question;
57
+ * `actors` and `graceDays` are only meaningful once it is on. `graceDays` is a
58
+ * whole number of days from zero (0 = permanent immediately) and is capped to
59
+ * keep a typo from writing a grace window measured in centuries.
60
+ */
61
+ const sealPolicySchema = Joi.object({
62
+ enabled: Joi.boolean().required(),
63
+ actors: Joi.string()
64
+ .valid('authorOrAdmin', 'author', 'admin', 'initiator', 'workflowMembers')
65
+ .optional(),
66
+ graceDays: Joi.number().integer().min(0).max(3650).optional(),
67
+ });
26
68
  /**
27
69
  * Validates a PERSISTED workflow, i.e. `DbWorkflowModel` — `_id` and the
28
70
  * timestamps are required here because this schema is only ever run against a
@@ -53,6 +95,12 @@ export const workflowModelSchema = Joi.object({
53
95
  // by the engine at transaction-create time, not by Mongo — see the
54
96
  // `uniqueKeys` note on `WorkflowModel`.
55
97
  uniqueKeys: Joi.array().items(Joi.string()).optional(),
98
+ // Whether archiving releases a `uniqueKeys` tuple (`'active'`, the pre-existing
99
+ // behaviour) or the tuple stays claimed for the life of the record (`'all'`).
100
+ // Read alongside `archivePolicy` — see both notes on `WorkflowModel`.
101
+ uniqueKeysScope: Joi.string().valid('active', 'all').optional(),
102
+ archivePolicy: archivePolicySchema.optional(),
103
+ sealPolicy: sealPolicySchema.optional(),
56
104
  });
57
105
  export function validateWorkflowModel(data) {
58
106
  const { error } = workflowModelSchema.validate(data, { abortEarly: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-t-workflow-typings",
3
- "version": "3.0.1",
3
+ "version": "3.2.0",
4
4
  "description": "Typings and interfaces for the ngx-t-workflows library.",
5
5
  "keywords": [
6
6
  "typings",
@@ -42,14 +42,14 @@
42
42
  "test": "echo \"Error: no test specified\" && exit 1"
43
43
  },
44
44
  "peerDependencies": {
45
- "ngx-t-forms-types": ">=0.0.16",
45
+ "ngx-t-forms-types": ">=0.0.27",
46
46
  "rxjs": ">=7.8.0"
47
47
  },
48
48
  "dependencies": {
49
49
  "joi": "^18.0.2"
50
50
  },
51
51
  "devDependencies": {
52
- "ngx-t-forms-types": "^0.0.16",
52
+ "ngx-t-forms-types": "^0.0.27",
53
53
  "rimraf": "^6.1.3",
54
54
  "typescript": "^5.9.3"
55
55
  }