@transitrix/diagrams 1.16.0 → 1.17.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.
@@ -6,7 +6,7 @@ export type ActorType = typeof ACTOR_TYPES[number];
6
6
  * inline on an ACTOR file (§7.10, ACTOR-003). They belong in time-aware REL
7
7
  * records, never on the identity primitive.
8
8
  */
9
- export declare const ACTOR_FORBIDDEN_INLINE_FIELDS: readonly ["employment", "candidacy", "alumni_membership", "community_membership", "contracting", "unit_parent", "located_at", "unit_located_at", "roles", "owner"];
9
+ export declare const ACTOR_FORBIDDEN_INLINE_FIELDS: readonly ["employment", "candidacy", "alumni_membership", "community_membership", "contracting", "unit_parent", "reports_to", "manager", "located_at", "unit_located_at", "roles", "owner"];
10
10
  export interface Actor {
11
11
  notation: 'actor';
12
12
  id: string;
@@ -13,6 +13,8 @@ export const ACTOR_FORBIDDEN_INLINE_FIELDS = [
13
13
  'community_membership',
14
14
  'contracting',
15
15
  'unit_parent',
16
+ 'reports_to',
17
+ 'manager',
16
18
  'located_at',
17
19
  'unit_located_at',
18
20
  'roles',
@@ -47,6 +47,7 @@ export interface GoalsViewConfig {
47
47
  }
48
48
  export interface GoalsCanonSources {
49
49
  elements: unknown[];
50
+ relations?: unknown[];
50
51
  }
51
52
  /**
52
53
  * Returns true when the parsed view document uses the canon-projection form
@@ -42,10 +42,51 @@ function collectGoalElements(docs) {
42
42
  if (!id)
43
43
  continue;
44
44
  if (!out.has(id))
45
- out.set(id, doc); // first definition wins
45
+ out.set(id, { ...doc }); // never mutate the caller's catalogue
46
46
  }
47
47
  return out;
48
48
  }
49
+ /** Materialize canonical child -> parent links for a single-parent tree.
50
+ * Never mutate canon or silently linearize an ambiguous hierarchy.
51
+ */
52
+ function applyGoalParentRelations(goals, relations, validAt) {
53
+ const declaredChildren = new Set();
54
+ const parents = new Map();
55
+ for (const doc of relations) {
56
+ if (!isObject(doc) || doc['notation'] !== 'relation' || doc['type'] !== 'goal_parent')
57
+ continue;
58
+ const child = str(doc['from']);
59
+ const parent = str(doc['to']);
60
+ if (!child || !parent || !goals.has(child) || !goals.has(parent)) {
61
+ throw new Error(`goal_parent relation ${str(doc['id']) ?? '(unnamed)'} must reference two existing goals`);
62
+ }
63
+ declaredChildren.add(child);
64
+ const from = str(doc['valid_from']);
65
+ const to = str(doc['valid_to']);
66
+ if (validAt && ((from && validAt < from) || (to && validAt > to)))
67
+ continue;
68
+ const choices = parents.get(child) ?? new Set();
69
+ choices.add(parent);
70
+ parents.set(child, choices);
71
+ }
72
+ for (const child of declaredChildren) {
73
+ const goal = goals.get(child);
74
+ const choices = parents.get(child) ?? new Set();
75
+ if (choices.size > 1) {
76
+ throw new Error(`goal_parent for ${child} has multiple parents in this scope; narrow valid_at or use a graph view`);
77
+ }
78
+ const parent = [...choices][0];
79
+ const legacyParent = str(goal['parent']);
80
+ if (parent && legacyParent && parent !== legacyParent) {
81
+ throw new Error(`goal_parent for ${child} conflicts with its inline parent`);
82
+ }
83
+ // Declared but inactive links must not fall back to an undated parent.
84
+ if (parent)
85
+ goal['parent'] = parent;
86
+ else
87
+ delete goal['parent'];
88
+ }
89
+ }
49
90
  /**
50
91
  * Returns true when the parsed view document uses the canon-projection form
51
92
  * (`view_config` key present, no inline `goals[]`). Used by the preview and
@@ -69,6 +110,8 @@ export function resolveGoals(viewDoc, sources) {
69
110
  const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
70
111
  const scope = isObject(vc['scope']) ? vc['scope'] : {};
71
112
  const allGoals = collectGoalElements(sources.elements);
113
+ const validAt = str(scope['valid_at']);
114
+ applyGoalParentRelations(allGoals, sources.relations ?? [], validAt);
72
115
  // 1. Scope by root_goal (this goal + its descendants), or every goal when
73
116
  // omitted (§5.1: "Omit to show all root goals").
74
117
  const rootGoal = str(scope['root_goal']);
@@ -97,7 +140,6 @@ export function resolveGoals(viewDoc, sources) {
97
140
  // 4. Narrow by valid_at — only goals in effect on that date. Goals with no
98
141
  // valid_from are not lifecycle-tracked and are kept (permissive default,
99
142
  // matching the action/dgca resolvers' stance).
100
- const validAt = str(scope['valid_at']);
101
143
  if (validAt) {
102
144
  candidateIds = new Set([...candidateIds].filter((id) => {
103
145
  const el = allGoals.get(id);
@@ -17,16 +17,11 @@ import { validateActionFields } from '../activities/action-fields.js';
17
17
  // classification (`Issue.Severity` in the Go source) and its "import
18
18
  // anyway, record the warning" policy.
19
19
  //
20
- // GOALS-009/011 (orphan / missing parent) are ported at warning severity even
21
- // though most standalone GOAL elements legitimately have no `parent` field —
22
- // GOAL.parent is "v0.x transitional" (ELEMENT_PRIMITIVES.md §7.2; canonical
23
- // home is a `goal_parent` REL file or the goals-tree view's inline `parent`).
24
- // `organizations/acme_corp`'s GOAL-CUST-1/GOAL-OPS-1/GOAL-EU-1 are exactly
25
- // this shape and do surface GOALS-011 warnings — that noise is the accepted
26
- // cost of a *warning* level rule (non-blocking, advisory), unlike the ERROR
27
- // tier this repo held the line on. This is a deliberate call, not an
28
- // oversight: keep the coverage rather than drop it, now that it can be
29
- // non-blocking.
20
+ // GOALS-010 checks canonical and legacy edges in overlapping lifecycle slices.
21
+ // GOALS-009/011 respect the canonical goal_parent records already loaded
22
+ // from canon/relations. Static catalogue coverage considers those links;
23
+ // date-scoped hierarchy selection and ambiguity are checked by Goals views.
24
+ // Legacy inline parents remain supported for transitional catalogues.
30
25
  //
31
26
  // GOALS-008 (type/level mismatch) is still NOT ported, at either severity.
32
27
  // Both of DSM's cases ("type not declared in goal_types" / "level doesn't
@@ -107,17 +102,15 @@ function collectByNotation(elements, matches) {
107
102
  }
108
103
  return out;
109
104
  }
110
- /** DFS cycle detection over a single-parent edge (GOAL.parent), matching DSM's
105
+ /** DFS cycle detection over resolved parent edges, matching DSM's
111
106
  * `findCycle` (goals.go): only walks an edge to a parent that itself resolves
112
107
  * within the set — an edge into an unresolved (orphan) parent is not walked,
113
108
  * so an orphan can never be mistaken for a cycle. Returns the id where a
114
109
  * back-edge was hit, or `undefined` if the graph is acyclic. */
115
- function findParentCycle(goals) {
116
- const parentOf = new Map();
110
+ function findParentCycle(goals, parents) {
117
111
  const known = new Set();
118
112
  for (const g of goals) {
119
113
  known.add(g.id);
120
- parentOf.set(g.id, readString(g.data, 'parent'));
121
114
  }
122
115
  const UNVISITED = 0;
123
116
  const VISITING = 1;
@@ -130,8 +123,9 @@ function findParentCycle(goals) {
130
123
  if (s === VISITING)
131
124
  return id;
132
125
  state.set(id, VISITING);
133
- const parent = parentOf.get(id);
134
- if (parent && known.has(parent)) {
126
+ for (const parent of parents.get(id) ?? []) {
127
+ if (!known.has(parent))
128
+ continue;
135
129
  const hit = walk(parent);
136
130
  if (hit)
137
131
  return hit;
@@ -184,27 +178,68 @@ function findPredecessorCycle(actions) {
184
178
  }
185
179
  return undefined;
186
180
  }
187
- /** GOALS-010 — the GOAL `parent` chain must not contain a cycle. */
188
- function checkGoalParentCycle(goals, findings) {
189
- const cyc = findParentCycle(goals);
190
- if (cyc) {
181
+ /** GOALS-010 — parent links must be acyclic in every effective date slice.
182
+ * A cycle can first appear only when an edge starts. Inspect those boundaries
183
+ * and the unbounded past; do not union disjoint historical parent relations.
184
+ */
185
+ function checkGoalParentCycle(goals, relations, findings) {
186
+ const known = new Set(goals.map(g => g.id));
187
+ const links = relations.flatMap(rel => {
188
+ const d = rel.data;
189
+ if (!d || d['notation'] !== 'relation' || d['type'] !== 'goal_parent')
190
+ return [];
191
+ const child = readString(d, 'from'), parent = readString(d, 'to');
192
+ if (!child || !parent || !known.has(child) || !known.has(parent))
193
+ return [];
194
+ return [{ child, parent, from: readString(d, 'valid_from'), to: readString(d, 'valid_to') }];
195
+ });
196
+ const declared = new Set(links.map(link => link.child));
197
+ const dates = [...new Set(['0000-01-01', ...links.flatMap(link => link.from ? [link.from] : [])])].sort();
198
+ for (const date of dates) {
199
+ const parents = new Map();
200
+ for (const g of goals) {
201
+ const legacy = readString(g.data, 'parent');
202
+ if (!declared.has(g.id) && legacy)
203
+ parents.set(g.id, [legacy]);
204
+ }
205
+ for (const link of links) {
206
+ if ((link.from && date < link.from) || (link.to && date > link.to))
207
+ continue;
208
+ parents.set(link.child, [...(parents.get(link.child) ?? []), link.parent]);
209
+ }
210
+ const cyc = findParentCycle(goals, parents);
211
+ if (!cyc)
212
+ continue;
191
213
  findings.push({
192
214
  scope: PScope,
193
215
  id: cyc,
194
216
  ruleId: 'GOALS-010',
195
217
  message: `GOALS-010: parent chain contains a cycle involving goal '${cyc}'.`,
196
218
  });
219
+ return;
197
220
  }
198
221
  }
199
222
  /** GOALS-009 / GOALS-011 — GOAL `parent` resolution (warning). Mirrors DSM's
200
223
  * goals.go: the two are mutually exclusive per goal — a goal with no
201
224
  * `parent` is checked for GOALS-011 (backlog, level >= 1); a goal with a
202
225
  * `parent` is checked for GOALS-009 (orphan, parent unresolved) instead. */
203
- function checkGoalParentResolution(goals, findings) {
226
+ function checkGoalParentResolution(goals, relations, findings) {
204
227
  const knownIds = new Set(goals.map((g) => g.id));
228
+ const linkedChildren = new Set();
229
+ for (const rel of relations) {
230
+ const data = rel.data;
231
+ if (!data || data['notation'] !== 'relation' || data['type'] !== 'goal_parent')
232
+ continue;
233
+ const child = readString(data, 'from');
234
+ const parent = readString(data, 'to');
235
+ if (child && parent && knownIds.has(child) && knownIds.has(parent))
236
+ linkedChildren.add(child);
237
+ }
205
238
  for (const g of goals) {
206
239
  const parent = readString(g.data, 'parent');
207
240
  if (!parent) {
241
+ if (linkedChildren.has(g.id))
242
+ continue;
208
243
  const level = readFiniteNumber(g.data, 'level');
209
244
  if (level !== undefined && level >= 1) {
210
245
  findings.push({
@@ -467,8 +502,8 @@ export function checkStrategyChainSemantics(input, findings) {
467
502
  const actions = collectByNotation(input.elements, isActionNotation);
468
503
  const drivers = collectByNotation(input.elements, isDriverNotation);
469
504
  const changes = collectByNotation(input.elements, isChangeNotation);
470
- checkGoalParentCycle(goals, findings);
471
- checkGoalParentResolution(goals, findings);
505
+ checkGoalParentCycle(goals, input.relations, findings);
506
+ checkGoalParentResolution(goals, input.relations, findings);
472
507
  checkActionSelfPredecessor(actions, findings);
473
508
  checkActionPredecessorCycle(actions, findings);
474
509
  checkActionOrphanReferences(actions, findings);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -18,6 +18,8 @@ export const ACTOR_FORBIDDEN_INLINE_FIELDS = [
18
18
  'community_membership',
19
19
  'contracting',
20
20
  'unit_parent',
21
+ 'reports_to',
22
+ 'manager',
21
23
  'located_at',
22
24
  'unit_located_at',
23
25
  'roles',
@@ -48,6 +48,7 @@ export interface GoalsViewConfig {
48
48
 
49
49
  export interface GoalsCanonSources {
50
50
  elements: unknown[];
51
+ relations?: unknown[];
51
52
  }
52
53
 
53
54
  /** GOAL elements by id (`notation: goal`). */
@@ -58,11 +59,53 @@ function collectGoalElements(docs: unknown[]): Map<string, Record<string, unknow
58
59
  if (str(doc['notation']) !== 'goal') continue;
59
60
  const id = str(doc['id']);
60
61
  if (!id) continue;
61
- if (!out.has(id)) out.set(id, doc); // first definition wins
62
+ if (!out.has(id)) out.set(id, { ...doc }); // never mutate the caller's catalogue
62
63
  }
63
64
  return out;
64
65
  }
65
66
 
67
+ /** Materialize canonical child -> parent links for a single-parent tree.
68
+ * Never mutate canon or silently linearize an ambiguous hierarchy.
69
+ */
70
+ function applyGoalParentRelations(
71
+ goals: Map<string, Record<string, unknown>>,
72
+ relations: unknown[],
73
+ validAt: string | undefined,
74
+ ): void {
75
+ const declaredChildren = new Set<string>();
76
+ const parents = new Map<string, Set<string>>();
77
+ for (const doc of relations) {
78
+ if (!isObject(doc) || doc['notation'] !== 'relation' || doc['type'] !== 'goal_parent') continue;
79
+ const child = str(doc['from']);
80
+ const parent = str(doc['to']);
81
+ if (!child || !parent || !goals.has(child) || !goals.has(parent)) {
82
+ throw new Error(`goal_parent relation ${str(doc['id']) ?? '(unnamed)'} must reference two existing goals`);
83
+ }
84
+ declaredChildren.add(child);
85
+ const from = str(doc['valid_from']);
86
+ const to = str(doc['valid_to']);
87
+ if (validAt && ((from && validAt < from) || (to && validAt > to))) continue;
88
+ const choices = parents.get(child) ?? new Set<string>();
89
+ choices.add(parent);
90
+ parents.set(child, choices);
91
+ }
92
+ for (const child of declaredChildren) {
93
+ const goal = goals.get(child)!;
94
+ const choices = parents.get(child) ?? new Set<string>();
95
+ if (choices.size > 1) {
96
+ throw new Error(`goal_parent for ${child} has multiple parents in this scope; narrow valid_at or use a graph view`);
97
+ }
98
+ const parent = [...choices][0];
99
+ const legacyParent = str(goal['parent']);
100
+ if (parent && legacyParent && parent !== legacyParent) {
101
+ throw new Error(`goal_parent for ${child} conflicts with its inline parent`);
102
+ }
103
+ // Declared but inactive links must not fall back to an undated parent.
104
+ if (parent) goal['parent'] = parent;
105
+ else delete goal['parent'];
106
+ }
107
+ }
108
+
66
109
  /**
67
110
  * Returns true when the parsed view document uses the canon-projection form
68
111
  * (`view_config` key present, no inline `goals[]`). Used by the preview and
@@ -91,6 +134,8 @@ export function resolveGoals(
91
134
  const scope = isObject(vc['scope']) ? vc['scope'] : {};
92
135
 
93
136
  const allGoals = collectGoalElements(sources.elements);
137
+ const validAt = str(scope['valid_at']);
138
+ applyGoalParentRelations(allGoals, sources.relations ?? [], validAt);
94
139
 
95
140
  // 1. Scope by root_goal (this goal + its descendants), or every goal when
96
141
  // omitted (§5.1: "Omit to show all root goals").
@@ -126,7 +171,6 @@ export function resolveGoals(
126
171
  // 4. Narrow by valid_at — only goals in effect on that date. Goals with no
127
172
  // valid_from are not lifecycle-tracked and are kept (permissive default,
128
173
  // matching the action/dgca resolvers' stance).
129
- const validAt = str(scope['valid_at']);
130
174
  if (validAt) {
131
175
  candidateIds = new Set(
132
176
  [...candidateIds].filter((id) => {
@@ -17,16 +17,11 @@ import { validateActionFields } from '../activities/action-fields.js';
17
17
  // classification (`Issue.Severity` in the Go source) and its "import
18
18
  // anyway, record the warning" policy.
19
19
  //
20
- // GOALS-009/011 (orphan / missing parent) are ported at warning severity even
21
- // though most standalone GOAL elements legitimately have no `parent` field —
22
- // GOAL.parent is "v0.x transitional" (ELEMENT_PRIMITIVES.md §7.2; canonical
23
- // home is a `goal_parent` REL file or the goals-tree view's inline `parent`).
24
- // `organizations/acme_corp`'s GOAL-CUST-1/GOAL-OPS-1/GOAL-EU-1 are exactly
25
- // this shape and do surface GOALS-011 warnings — that noise is the accepted
26
- // cost of a *warning* level rule (non-blocking, advisory), unlike the ERROR
27
- // tier this repo held the line on. This is a deliberate call, not an
28
- // oversight: keep the coverage rather than drop it, now that it can be
29
- // non-blocking.
20
+ // GOALS-010 checks canonical and legacy edges in overlapping lifecycle slices.
21
+ // GOALS-009/011 respect the canonical goal_parent records already loaded
22
+ // from canon/relations. Static catalogue coverage considers those links;
23
+ // date-scoped hierarchy selection and ambiguity are checked by Goals views.
24
+ // Legacy inline parents remain supported for transitional catalogues.
30
25
  //
31
26
  // GOALS-008 (type/level mismatch) is still NOT ported, at either severity.
32
27
  // Both of DSM's cases ("type not declared in goal_types" / "level doesn't
@@ -124,17 +119,15 @@ function collectByNotation(
124
119
  return out;
125
120
  }
126
121
 
127
- /** DFS cycle detection over a single-parent edge (GOAL.parent), matching DSM's
122
+ /** DFS cycle detection over resolved parent edges, matching DSM's
128
123
  * `findCycle` (goals.go): only walks an edge to a parent that itself resolves
129
124
  * within the set — an edge into an unresolved (orphan) parent is not walked,
130
125
  * so an orphan can never be mistaken for a cycle. Returns the id where a
131
126
  * back-edge was hit, or `undefined` if the graph is acyclic. */
132
- function findParentCycle(goals: ChainElement[]): string | undefined {
133
- const parentOf = new Map<string, string | undefined>();
127
+ function findParentCycle(goals: ChainElement[], parents: Map<string, string[]>): string | undefined {
134
128
  const known = new Set<string>();
135
129
  for (const g of goals) {
136
130
  known.add(g.id);
137
- parentOf.set(g.id, readString(g.data, 'parent'));
138
131
  }
139
132
  const UNVISITED = 0;
140
133
  const VISITING = 1;
@@ -146,8 +139,8 @@ function findParentCycle(goals: ChainElement[]): string | undefined {
146
139
  if (s === DONE) return undefined;
147
140
  if (s === VISITING) return id;
148
141
  state.set(id, VISITING);
149
- const parent = parentOf.get(id);
150
- if (parent && known.has(parent)) {
142
+ for (const parent of parents.get(id) ?? []) {
143
+ if (!known.has(parent)) continue;
151
144
  const hit = walk(parent);
152
145
  if (hit) return hit;
153
146
  }
@@ -198,16 +191,40 @@ function findPredecessorCycle(actions: ChainElement[]): string | undefined {
198
191
  return undefined;
199
192
  }
200
193
 
201
- /** GOALS-010 — the GOAL `parent` chain must not contain a cycle. */
202
- function checkGoalParentCycle(goals: ChainElement[], findings: RepoFinding[]): void {
203
- const cyc = findParentCycle(goals);
204
- if (cyc) {
194
+ /** GOALS-010 — parent links must be acyclic in every effective date slice.
195
+ * A cycle can first appear only when an edge starts. Inspect those boundaries
196
+ * and the unbounded past; do not union disjoint historical parent relations.
197
+ */
198
+ function checkGoalParentCycle(goals: ChainElement[], relations: RepoDoc[], findings: RepoFinding[]): void {
199
+ const known = new Set(goals.map(g => g.id));
200
+ const links = relations.flatMap(rel => {
201
+ const d = rel.data;
202
+ if (!d || d['notation'] !== 'relation' || d['type'] !== 'goal_parent') return [];
203
+ const child = readString(d, 'from'), parent = readString(d, 'to');
204
+ if (!child || !parent || !known.has(child) || !known.has(parent)) return [];
205
+ return [{ child, parent, from: readString(d, 'valid_from'), to: readString(d, 'valid_to') }];
206
+ });
207
+ const declared = new Set(links.map(link => link.child));
208
+ const dates = [...new Set(['0000-01-01', ...links.flatMap(link => link.from ? [link.from] : [])])].sort();
209
+ for (const date of dates) {
210
+ const parents = new Map<string, string[]>();
211
+ for (const g of goals) {
212
+ const legacy = readString(g.data, 'parent');
213
+ if (!declared.has(g.id) && legacy) parents.set(g.id, [legacy]);
214
+ }
215
+ for (const link of links) {
216
+ if ((link.from && date < link.from) || (link.to && date > link.to)) continue;
217
+ parents.set(link.child, [...(parents.get(link.child) ?? []), link.parent]);
218
+ }
219
+ const cyc = findParentCycle(goals, parents);
220
+ if (!cyc) continue;
205
221
  findings.push({
206
222
  scope: PScope,
207
223
  id: cyc,
208
224
  ruleId: 'GOALS-010',
209
225
  message: `GOALS-010: parent chain contains a cycle involving goal '${cyc}'.`,
210
226
  });
227
+ return;
211
228
  }
212
229
  }
213
230
 
@@ -215,11 +232,20 @@ function checkGoalParentCycle(goals: ChainElement[], findings: RepoFinding[]): v
215
232
  * goals.go: the two are mutually exclusive per goal — a goal with no
216
233
  * `parent` is checked for GOALS-011 (backlog, level >= 1); a goal with a
217
234
  * `parent` is checked for GOALS-009 (orphan, parent unresolved) instead. */
218
- function checkGoalParentResolution(goals: ChainElement[], findings: RepoFinding[]): void {
235
+ function checkGoalParentResolution(goals: ChainElement[], relations: RepoDoc[], findings: RepoFinding[]): void {
219
236
  const knownIds = new Set(goals.map((g) => g.id));
237
+ const linkedChildren = new Set<string>();
238
+ for (const rel of relations) {
239
+ const data = rel.data;
240
+ if (!data || data['notation'] !== 'relation' || data['type'] !== 'goal_parent') continue;
241
+ const child = readString(data, 'from');
242
+ const parent = readString(data, 'to');
243
+ if (child && parent && knownIds.has(child) && knownIds.has(parent)) linkedChildren.add(child);
244
+ }
220
245
  for (const g of goals) {
221
246
  const parent = readString(g.data, 'parent');
222
247
  if (!parent) {
248
+ if (linkedChildren.has(g.id)) continue;
223
249
  const level = readFiniteNumber(g.data, 'level');
224
250
  if (level !== undefined && level >= 1) {
225
251
  findings.push({
@@ -504,8 +530,8 @@ export function checkStrategyChainSemantics(input: RepoModelInput, findings: Rep
504
530
  const drivers = collectByNotation(input.elements, isDriverNotation);
505
531
  const changes = collectByNotation(input.elements, isChangeNotation);
506
532
 
507
- checkGoalParentCycle(goals, findings);
508
- checkGoalParentResolution(goals, findings);
533
+ checkGoalParentCycle(goals, input.relations, findings);
534
+ checkGoalParentResolution(goals, input.relations, findings);
509
535
  checkActionSelfPredecessor(actions, findings);
510
536
  checkActionPredecessorCycle(actions, findings);
511
537
  checkActionOrphanReferences(actions, findings);