@transitrix/diagrams 1.8.13 → 1.8.19

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.
Files changed (45) hide show
  1. package/dist/activities/validate.js +9 -1
  2. package/dist/blocks/index.d.ts +5 -4
  3. package/dist/blocks/index.js +3 -2
  4. package/dist/blocks/layout.d.ts +8 -1
  5. package/dist/blocks/layout.js +57 -0
  6. package/dist/blocks/templates/index.d.ts +9 -0
  7. package/dist/blocks/templates/index.js +11 -0
  8. package/dist/blocks/templates/raci.d.ts +9 -0
  9. package/dist/blocks/templates/raci.js +24 -0
  10. package/dist/blocks/types.d.ts +58 -0
  11. package/dist/blocks/validate.d.ts +31 -1
  12. package/dist/blocks/validate.js +127 -0
  13. package/dist/capability-map/validate.js +41 -0
  14. package/dist/edge-path.d.ts +15 -0
  15. package/dist/edge-path.js +19 -0
  16. package/dist/fgca/parse-canonical.js +8 -0
  17. package/dist/repo-validate/check-element-hygiene.d.ts +8 -0
  18. package/dist/repo-validate/check-element-hygiene.js +211 -0
  19. package/dist/repo-validate/check-strategy-chain.d.ts +8 -0
  20. package/dist/repo-validate/check-strategy-chain.js +491 -0
  21. package/dist/repo-validate/types.d.ts +14 -1
  22. package/dist/repo-validate/types.js +22 -3
  23. package/dist/repo-validate/validate-repo.d.ts +3 -2
  24. package/dist/repo-validate/validate-repo.js +16 -4
  25. package/dist/webview/render-activities.d.ts +9 -0
  26. package/dist/webview/render-activities.js +72 -4
  27. package/dist/webview/render-blocks.d.ts +23 -1
  28. package/dist/webview/render-blocks.js +100 -2
  29. package/package.json +1 -1
  30. package/src/activities/validate.ts +9 -1
  31. package/src/blocks/index.ts +16 -2
  32. package/src/blocks/layout.ts +70 -0
  33. package/src/blocks/templates/index.ts +13 -0
  34. package/src/blocks/templates/raci.ts +28 -0
  35. package/src/blocks/types.ts +67 -0
  36. package/src/blocks/validate.ts +157 -1
  37. package/src/capability-map/validate.ts +40 -0
  38. package/src/edge-path.ts +27 -0
  39. package/src/fgca/parse-canonical.ts +8 -0
  40. package/src/repo-validate/check-element-hygiene.ts +236 -0
  41. package/src/repo-validate/check-strategy-chain.ts +528 -0
  42. package/src/repo-validate/types.ts +36 -4
  43. package/src/repo-validate/validate-repo.ts +16 -4
  44. package/src/webview/render-activities.ts +76 -4
  45. package/src/webview/render-blocks.ts +159 -2
@@ -0,0 +1,491 @@
1
+ // Strategy-chain semantic checks — the error-severity subset of DSM's Go
2
+ // `Validate*` functions (`api02/internal/importer/{goals,activities,fgca}.go`
3
+ // in transitrix-dsm), ported onto the standalone-element repo shape
4
+ // (`canon/elements/**`) that `validate --scope=repo` already loads, so DSM
5
+ // can drop those Go functions without regressing the checks they enforce
6
+ // today.
7
+ //
8
+ // Rule codes are DSM's own (`GOALS-010`, `ACT-006`..`009`, `FGCA-008`..`011`)
9
+ // — not invented here — so DSM can map a CLI finding straight back onto its
10
+ // import-log taxonomy (`RepoFinding.ruleId`).
11
+ //
12
+ // Scope — DSM's full rule set is now ported except GOALS-008 (see below).
13
+ // `RepoFinding` grew a `severity` field precisely so DSM's warn-severity
14
+ // rules could land without becoming blocking: every finding from before that
15
+ // field existed stays implicitly `'error'` (`types.ts`), and the six rules
16
+ // below set `severity: 'warning'` explicitly, matching DSM's own
17
+ // classification (`Issue.Severity` in the Go source) and its "import
18
+ // anyway, record the warning" policy.
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.
30
+ //
31
+ // GOALS-008 (type/level mismatch) is still NOT ported, at either severity.
32
+ // Both of DSM's cases ("type not declared in goal_types" / "level doesn't
33
+ // match the type's declared level") need the `goal_types[]` catalogue, which
34
+ // lives on the goals-tree *view* (`canon/views/goals/**`,
35
+ // notations/views/04-goals.md §5.2) — a zone this validator's `RepoModelInput`
36
+ // does not load (only `canon/elements/**` and `canon/relations/**`, per
37
+ // `validate-repo.ts`). There is no standalone-element-shape data this rule
38
+ // could run against without the validator growing a third input zone — a
39
+ // bigger architectural change than adapting a predicate to data already in
40
+ // scope, unlike every other ported rule here. This is flagged for a
41
+ // per-rule call: skip it (status quo) or scope a follow-up that loads
42
+ // `canon/views/goals/**`'s `goal_types[]` into the repo-scope model.
43
+ //
44
+ // Ported (error-severity, blocking):
45
+ // GOALS-010 — GOAL `parent` chain contains a cycle.
46
+ // ACT-006 — ACTION `predecessors` graph contains a cycle.
47
+ // ACT-007 — ACTION lists itself as its own predecessor.
48
+ // ACT-008 — ACTION `start_date`/`end_date` unparseable, or end before start.
49
+ // ACT-009 — ACTION numeric field (`duration`/`duration_days`, `labor_cost`,
50
+ // `resources_cost`, `effort`, `score`) is negative.
51
+ // FGCA-008 — GOAL.factors references an undefined DRIVER.
52
+ // FGCA-009 — CHANGE.goals references an undefined GOAL.
53
+ // FGCA-010 — ACTION.delivers_changes references an undefined CHANGE.
54
+ // FGCA-011 — ACTION.goals references an undefined GOAL.
55
+ //
56
+ // Ported (warning-severity, advisory):
57
+ // GOALS-009 — GOAL.parent is set but does not resolve to a known GOAL (orphan).
58
+ // GOALS-011 — GOAL has no `parent` and `level` >= 1 (backlog).
59
+ // ACT-005 — ACTION.predecessors entry or ACTION.parent does not resolve
60
+ // to a known ACTION (orphan).
61
+ // FGCA-012 — a DRIVER is not referenced by any GOAL.factors (unreferenced).
62
+ // FGCA-013 — a GOAL is not referenced by any CHANGE.goals or ACTION.goals
63
+ // (unreferenced).
64
+ // FGCA-014 — a CHANGE is not referenced by any ACTION.delivers_changes
65
+ // (unreferenced).
66
+ import { docId } from './validate-repo.js';
67
+ const PScope = 'repo';
68
+ function isGoalNotation(n) {
69
+ return n === 'goal';
70
+ }
71
+ function isActionNotation(n) {
72
+ // 'activity' is the deprecated pre-2026-06-25 alias (elements/24-action.md §5).
73
+ return n === 'action' || n === 'activity';
74
+ }
75
+ function isDriverNotation(n) {
76
+ // 'factor' is the pre-rename notation value (Factor -> Driver rename, in progress).
77
+ return n === 'driver' || n === 'factor';
78
+ }
79
+ function isChangeNotation(n) {
80
+ return n === 'change';
81
+ }
82
+ function readStringArray(data, key) {
83
+ const v = data[key];
84
+ if (!Array.isArray(v))
85
+ return [];
86
+ return v.filter((x) => typeof x === 'string' && x.length > 0);
87
+ }
88
+ function readString(data, key) {
89
+ const v = data[key];
90
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
91
+ }
92
+ function readFiniteNumber(data, key) {
93
+ const v = data[key];
94
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
95
+ }
96
+ function collectByNotation(elements, matches) {
97
+ const out = [];
98
+ for (const doc of elements) {
99
+ if (!doc.data)
100
+ continue;
101
+ if (!matches(doc.data['notation']))
102
+ continue;
103
+ const id = docId(doc);
104
+ if (!id)
105
+ continue;
106
+ out.push({ id, doc, data: doc.data });
107
+ }
108
+ return out;
109
+ }
110
+ /** DFS cycle detection over a single-parent edge (GOAL.parent), matching DSM's
111
+ * `findCycle` (goals.go): only walks an edge to a parent that itself resolves
112
+ * within the set — an edge into an unresolved (orphan) parent is not walked,
113
+ * so an orphan can never be mistaken for a cycle. Returns the id where a
114
+ * back-edge was hit, or `undefined` if the graph is acyclic. */
115
+ function findParentCycle(goals) {
116
+ const parentOf = new Map();
117
+ const known = new Set();
118
+ for (const g of goals) {
119
+ known.add(g.id);
120
+ parentOf.set(g.id, readString(g.data, 'parent'));
121
+ }
122
+ const UNVISITED = 0;
123
+ const VISITING = 1;
124
+ const DONE = 2;
125
+ const state = new Map();
126
+ function walk(id) {
127
+ const s = state.get(id) ?? UNVISITED;
128
+ if (s === DONE)
129
+ return undefined;
130
+ if (s === VISITING)
131
+ return id;
132
+ state.set(id, VISITING);
133
+ const parent = parentOf.get(id);
134
+ if (parent && known.has(parent)) {
135
+ const hit = walk(parent);
136
+ if (hit)
137
+ return hit;
138
+ }
139
+ state.set(id, DONE);
140
+ return undefined;
141
+ }
142
+ for (const g of goals) {
143
+ const hit = walk(g.id);
144
+ if (hit)
145
+ return hit;
146
+ }
147
+ return undefined;
148
+ }
149
+ /** DFS cycle detection over a multi-predecessor edge list (ACTION.predecessors),
150
+ * matching DSM's `findActivityCycle` (activities.go). Unresolved predecessors
151
+ * are skipped, same rationale as `findParentCycle`. */
152
+ function findPredecessorCycle(actions) {
153
+ const predsOf = new Map();
154
+ const known = new Set();
155
+ for (const a of actions) {
156
+ known.add(a.id);
157
+ predsOf.set(a.id, readStringArray(a.data, 'predecessors'));
158
+ }
159
+ const UNVISITED = 0;
160
+ const VISITING = 1;
161
+ const DONE = 2;
162
+ const state = new Map();
163
+ function walk(id) {
164
+ const s = state.get(id) ?? UNVISITED;
165
+ if (s === DONE)
166
+ return undefined;
167
+ if (s === VISITING)
168
+ return id;
169
+ state.set(id, VISITING);
170
+ for (const p of predsOf.get(id) ?? []) {
171
+ if (!known.has(p))
172
+ continue;
173
+ const hit = walk(p);
174
+ if (hit)
175
+ return hit;
176
+ }
177
+ state.set(id, DONE);
178
+ return undefined;
179
+ }
180
+ for (const a of actions) {
181
+ const hit = walk(a.id);
182
+ if (hit)
183
+ return hit;
184
+ }
185
+ return undefined;
186
+ }
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) {
191
+ findings.push({
192
+ scope: PScope,
193
+ id: cyc,
194
+ ruleId: 'GOALS-010',
195
+ message: `GOALS-010: parent chain contains a cycle involving goal '${cyc}'.`,
196
+ });
197
+ }
198
+ }
199
+ /** GOALS-009 / GOALS-011 — GOAL `parent` resolution (warning). Mirrors DSM's
200
+ * goals.go: the two are mutually exclusive per goal — a goal with no
201
+ * `parent` is checked for GOALS-011 (backlog, level >= 1); a goal with a
202
+ * `parent` is checked for GOALS-009 (orphan, parent unresolved) instead. */
203
+ function checkGoalParentResolution(goals, findings) {
204
+ const knownIds = new Set(goals.map((g) => g.id));
205
+ for (const g of goals) {
206
+ const parent = readString(g.data, 'parent');
207
+ if (!parent) {
208
+ const level = readFiniteNumber(g.data, 'level');
209
+ if (level !== undefined && level >= 1) {
210
+ findings.push({
211
+ scope: PScope,
212
+ id: g.id,
213
+ ruleId: 'GOALS-011',
214
+ severity: 'warning',
215
+ message: `GOALS-011: goal '${g.id}' (level ${level}) has no parent; treated as backlog until attached.`,
216
+ });
217
+ }
218
+ continue;
219
+ }
220
+ if (!knownIds.has(parent)) {
221
+ findings.push({
222
+ scope: PScope,
223
+ id: g.id,
224
+ ruleId: 'GOALS-009',
225
+ severity: 'warning',
226
+ message: `GOALS-009: goal '${g.id}' parent '${parent}' does not resolve to a known goal; treated as orphan.`,
227
+ });
228
+ }
229
+ }
230
+ }
231
+ /** ACT-007 — an ACTION cannot list itself as its own predecessor. */
232
+ function checkActionSelfPredecessor(actions, findings) {
233
+ for (const a of actions) {
234
+ if (readStringArray(a.data, 'predecessors').includes(a.id)) {
235
+ findings.push({
236
+ scope: PScope,
237
+ id: a.id,
238
+ ruleId: 'ACT-007',
239
+ message: `ACT-007: action '${a.id}' lists itself as a predecessor.`,
240
+ });
241
+ }
242
+ }
243
+ }
244
+ /** ACT-006 — the ACTION `predecessors` graph must not contain a cycle. */
245
+ function checkActionPredecessorCycle(actions, findings) {
246
+ const cyc = findPredecessorCycle(actions);
247
+ if (cyc) {
248
+ findings.push({
249
+ scope: PScope,
250
+ id: cyc,
251
+ ruleId: 'ACT-006',
252
+ message: `ACT-006: predecessor graph contains a cycle involving action '${cyc}'.`,
253
+ });
254
+ }
255
+ }
256
+ /** ACT-005 — an ACTION `predecessors` entry or `parent` that does not resolve
257
+ * to a known ACTION is an orphan reference (warning — advisory in DSM, same
258
+ * as the goal-parent orphan checks above). */
259
+ function checkActionOrphanReferences(actions, findings) {
260
+ const knownIds = new Set(actions.map((a) => a.id));
261
+ for (const a of actions) {
262
+ for (const p of readStringArray(a.data, 'predecessors')) {
263
+ if (!knownIds.has(p)) {
264
+ findings.push({
265
+ scope: PScope,
266
+ id: a.id,
267
+ ruleId: 'ACT-005',
268
+ severity: 'warning',
269
+ message: `ACT-005: action '${a.id}' predecessor '${p}' does not resolve to a known action.`,
270
+ });
271
+ }
272
+ }
273
+ const parent = readString(a.data, 'parent');
274
+ if (parent && !knownIds.has(parent)) {
275
+ findings.push({
276
+ scope: PScope,
277
+ id: a.id,
278
+ ruleId: 'ACT-005',
279
+ severity: 'warning',
280
+ message: `ACT-005: action '${a.id}' parent '${parent}' does not resolve to a known action.`,
281
+ });
282
+ }
283
+ }
284
+ }
285
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
286
+ /** Parse a strict ISO `YYYY-MM-DD` date, rejecting calendar-invalid dates
287
+ * (e.g. `2026-02-30`) the way Go's `time.Parse(dateLayout, …)` does — a
288
+ * regex match alone accepts those. Returns `undefined` when invalid. */
289
+ function parseIsoDate(value) {
290
+ if (!ISO_DATE_RE.test(value))
291
+ return undefined;
292
+ const [y, m, d] = value.split('-').map(Number);
293
+ const dt = new Date(Date.UTC(y, m - 1, d));
294
+ if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d) {
295
+ return undefined;
296
+ }
297
+ return dt;
298
+ }
299
+ /** ACT-008 — `start_date`/`end_date` must be valid ISO dates, and `end_date`
300
+ * must not be before `start_date` (equal is allowed — e.g. a milestone). */
301
+ function checkActionDates(actions, findings) {
302
+ for (const a of actions) {
303
+ const startRaw = readString(a.data, 'start_date');
304
+ const endRaw = readString(a.data, 'end_date');
305
+ let start;
306
+ let end;
307
+ if (startRaw !== undefined) {
308
+ start = parseIsoDate(startRaw);
309
+ if (!start) {
310
+ findings.push({
311
+ scope: PScope,
312
+ id: a.id,
313
+ ruleId: 'ACT-008',
314
+ message: `ACT-008: action '${a.id}' start_date '${startRaw}' is not a valid YYYY-MM-DD date.`,
315
+ });
316
+ }
317
+ }
318
+ if (endRaw !== undefined) {
319
+ end = parseIsoDate(endRaw);
320
+ if (!end) {
321
+ findings.push({
322
+ scope: PScope,
323
+ id: a.id,
324
+ ruleId: 'ACT-008',
325
+ message: `ACT-008: action '${a.id}' end_date '${endRaw}' is not a valid YYYY-MM-DD date.`,
326
+ });
327
+ }
328
+ }
329
+ if (start && end && end.getTime() < start.getTime()) {
330
+ findings.push({
331
+ scope: PScope,
332
+ id: a.id,
333
+ ruleId: 'ACT-008',
334
+ message: `ACT-008: action '${a.id}' end_date '${endRaw}' is before start_date '${startRaw}'.`,
335
+ });
336
+ }
337
+ }
338
+ }
339
+ /** ACT-009 — numeric scheduling/cost fields must not be negative. `duration`
340
+ * and `duration_days` are both checked — the canonical field is `duration`
341
+ * (elements/24-action.md §2), but `duration_days` is an accepted alias in
342
+ * this codebase's document-form validator (activities/validate.ts) and is
343
+ * the field acme_corp's own ACTION elements actually use. */
344
+ function checkActionNegativeNumbers(actions, findings) {
345
+ const fields = ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score'];
346
+ for (const a of actions) {
347
+ for (const field of fields) {
348
+ const v = readFiniteNumber(a.data, field);
349
+ if (v !== undefined && v < 0) {
350
+ findings.push({
351
+ scope: PScope,
352
+ id: a.id,
353
+ ruleId: 'ACT-009',
354
+ message: `ACT-009: action '${a.id}' field '${field}' is negative (${v}).`,
355
+ });
356
+ }
357
+ }
358
+ }
359
+ }
360
+ /** FGCA-008..011 — inline strategy-chain cross-references must resolve within
361
+ * the repo: GOAL.factors -> DRIVER, CHANGE.goals -> GOAL, ACTION.goals ->
362
+ * GOAL, ACTION.delivers_changes -> CHANGE (ELEMENT_PRIMITIVES.md §7.1-§7.4). */
363
+ function checkStrategyChainReferences(goals, actions, drivers, changes, findings) {
364
+ const driverIds = new Set(drivers.map((d) => d.id));
365
+ const goalIds = new Set(goals.map((g) => g.id));
366
+ const changeIds = new Set(changes.map((c) => c.id));
367
+ for (const g of goals) {
368
+ for (const f of readStringArray(g.data, 'factors')) {
369
+ if (!driverIds.has(f)) {
370
+ findings.push({
371
+ scope: PScope,
372
+ id: g.id,
373
+ ruleId: 'FGCA-008',
374
+ message: `FGCA-008: goal '${g.id}' references undefined driver '${f}'.`,
375
+ });
376
+ }
377
+ }
378
+ }
379
+ for (const c of changes) {
380
+ for (const g of readStringArray(c.data, 'goals')) {
381
+ if (!goalIds.has(g)) {
382
+ findings.push({
383
+ scope: PScope,
384
+ id: c.id,
385
+ ruleId: 'FGCA-009',
386
+ message: `FGCA-009: change '${c.id}' references undefined goal '${g}'.`,
387
+ });
388
+ }
389
+ }
390
+ }
391
+ for (const a of actions) {
392
+ for (const c of readStringArray(a.data, 'delivers_changes')) {
393
+ if (!changeIds.has(c)) {
394
+ findings.push({
395
+ scope: PScope,
396
+ id: a.id,
397
+ ruleId: 'FGCA-010',
398
+ message: `FGCA-010: action '${a.id}' references undefined change '${c}'.`,
399
+ });
400
+ }
401
+ }
402
+ for (const g of readStringArray(a.data, 'goals')) {
403
+ if (!goalIds.has(g)) {
404
+ findings.push({
405
+ scope: PScope,
406
+ id: a.id,
407
+ ruleId: 'FGCA-011',
408
+ message: `FGCA-011: action '${a.id}' references undefined goal '${g}'.`,
409
+ });
410
+ }
411
+ }
412
+ }
413
+ }
414
+ /** FGCA-012..014 — a DRIVER/GOAL/CHANGE defined but never referenced
415
+ * downstream in the strategy chain is unreferenced (warning — advisory in
416
+ * DSM, mirroring the orphan-reference checks above). */
417
+ function checkStrategyChainOrphans(goals, actions, drivers, changes, findings) {
418
+ const referencedDrivers = new Set();
419
+ for (const g of goals) {
420
+ for (const f of readStringArray(g.data, 'factors'))
421
+ referencedDrivers.add(f);
422
+ }
423
+ for (const d of drivers) {
424
+ if (!referencedDrivers.has(d.id)) {
425
+ findings.push({
426
+ scope: PScope,
427
+ id: d.id,
428
+ ruleId: 'FGCA-012',
429
+ severity: 'warning',
430
+ message: `FGCA-012: driver '${d.id}' is not referenced by any goal.`,
431
+ });
432
+ }
433
+ }
434
+ const referencedGoals = new Set();
435
+ for (const c of changes) {
436
+ for (const g of readStringArray(c.data, 'goals'))
437
+ referencedGoals.add(g);
438
+ }
439
+ for (const a of actions) {
440
+ for (const g of readStringArray(a.data, 'goals'))
441
+ referencedGoals.add(g);
442
+ }
443
+ for (const g of goals) {
444
+ if (!referencedGoals.has(g.id)) {
445
+ findings.push({
446
+ scope: PScope,
447
+ id: g.id,
448
+ ruleId: 'FGCA-013',
449
+ severity: 'warning',
450
+ message: `FGCA-013: goal '${g.id}' is not referenced by any change or action.`,
451
+ });
452
+ }
453
+ }
454
+ const referencedChanges = new Set();
455
+ for (const a of actions) {
456
+ for (const c of readStringArray(a.data, 'delivers_changes'))
457
+ referencedChanges.add(c);
458
+ }
459
+ for (const c of changes) {
460
+ if (!referencedChanges.has(c.id)) {
461
+ findings.push({
462
+ scope: PScope,
463
+ id: c.id,
464
+ ruleId: 'FGCA-014',
465
+ severity: 'warning',
466
+ message: `FGCA-014: change '${c.id}' is not referenced by any action.`,
467
+ });
468
+ }
469
+ }
470
+ }
471
+ /**
472
+ * Run the strategy-chain semantic checks (GOALS-009..011, ACT-005..009,
473
+ * FGCA-008..014 except GOALS-008 — see the module header) over the loaded
474
+ * element set and append findings. Called from `validateRepoModel` after the
475
+ * structural phases. Pure, deterministic order.
476
+ */
477
+ export function checkStrategyChainSemantics(input, findings) {
478
+ const goals = collectByNotation(input.elements, isGoalNotation);
479
+ const actions = collectByNotation(input.elements, isActionNotation);
480
+ const drivers = collectByNotation(input.elements, isDriverNotation);
481
+ const changes = collectByNotation(input.elements, isChangeNotation);
482
+ checkGoalParentCycle(goals, findings);
483
+ checkGoalParentResolution(goals, findings);
484
+ checkActionSelfPredecessor(actions, findings);
485
+ checkActionPredecessorCycle(actions, findings);
486
+ checkActionOrphanReferences(actions, findings);
487
+ checkActionDates(actions, findings);
488
+ checkActionNegativeNumbers(actions, findings);
489
+ checkStrategyChainReferences(goals, actions, drivers, changes, findings);
490
+ checkStrategyChainOrphans(goals, actions, drivers, changes, findings);
491
+ }
@@ -1,4 +1,4 @@
1
- /** A single repo-scope validation finding. Shape is frozen at `{ scope, id, message }`. */
1
+ /** A single repo-scope validation finding. */
2
2
  export interface RepoFinding {
3
3
  /** Always `'repo'` — the execution axis this finding came from. */
4
4
  scope: 'repo';
@@ -9,6 +9,19 @@ export interface RepoFinding {
9
9
  id: string;
10
10
  /** Human-readable description of the violation. */
11
11
  message: string;
12
+ /**
13
+ * Stable rule code (e.g. `GOALS-010`, `ACT-006`, `FGCA-008`), for a consumer
14
+ * that maps findings onto its own rule taxonomy (`docs/validation.md`).
15
+ * Omitted for checks that don't yet have one.
16
+ */
17
+ ruleId?: string;
18
+ /**
19
+ * `'error'` (blocking) or `'warning'` (advisory). Omitted is equivalent to
20
+ * `'error'` — every check that predates this field never sets it and must
21
+ * stay blocking. Only the warn-severity strategy-chain rules set this to
22
+ * `'warning'` today (`docs/validation.md` § repo-scope rule list).
23
+ */
24
+ severity?: 'error' | 'warning';
12
25
  }
13
26
  /** A parsed canon document fed to the repo validator. IO (the filesystem walk)
14
27
  * lives in the CLI; the validator itself stays pure and testable. */
@@ -7,7 +7,26 @@
7
7
  // runtime (methodology ADR "Validation Two-Axis Model"; Studio decision
8
8
  // "Validation Runtime Convergence").
9
9
  //
10
- // The finding shape is deliberately minimal — `{ scope, id, message }`. The
11
- // richer `target` / `category` reporting taxonomy is explicitly DEFERRED per the
12
- // ADR until a real consumer needs it; do not add it here.
10
+ // The finding shape was originally frozen minimal — `{ scope, id, message }` —
11
+ // with the richer `target` / `category` reporting taxonomy explicitly DEFERRED
12
+ // per the ADR until a real consumer needed it. This un-freezes one field,
13
+ // `ruleId`: DSM (migrating off its own Go `Validate*` functions) needs a
14
+ // stable code per finding to map onto its existing import-log rule taxonomy
15
+ // (`api02/internal/importer/{goals,activities,fgca}.go`). The richer
16
+ // `target`/`category` taxonomy stays deferred — only `ruleId` is added.
17
+ // `ruleId` is optional: only checks with a stable, documented code set it
18
+ // (`docs/validation.md` § repo-scope rule list); the original structural
19
+ // checks (syntax/uniqueness/atomicity/referential-integrity/policy) stay
20
+ // uncoded rather than being retrofitted with invented codes.
21
+ //
22
+ // A second field, `severity`, is un-frozen for the same reason: DSM's
23
+ // warn-severity strategy-chain rules (GOALS-009/011, ACT-005, FGCA-012..014
24
+ // — see check-strategy-chain.ts) are non-fatal by
25
+ // DSM's own taxonomy ("import anyway, record the warning"); without a
26
+ // severity field, porting them here would silently make them blocking,
27
+ // since every existing repo-scope finding is implicitly blocking. `severity`
28
+ // is optional and defaults to `'error'` when omitted — every finding from
29
+ // before this field existed (syntax/uniqueness/atomicity/referential-
30
+ // integrity/policy, plus the error-tier strategy-chain rules and
31
+ // TSVC-003/INT-002) stays blocking with no code change at the call site.
13
32
  export {};
@@ -12,7 +12,8 @@ export declare function endpointId(value: unknown): string | null;
12
12
  /**
13
13
  * Run all repo-scope checks over a loaded canon model and return the findings.
14
14
  * Pure: no IO, deterministic order (syntax, uniqueness, atomicity, referential,
15
- * semantics, policy). Reaches parity with `lint.py` on the `acme_corp` fixture
16
- * (zero findings on a clean tree).
15
+ * semantics, policy, strategy-chain). Reaches parity with `lint.py` on the
16
+ * `acme_corp` fixture (zero findings on a clean tree from the original
17
+ * lint.py-ported phases).
17
18
  */
18
19
  export declare function validateRepoModel(input: RepoModelInput): RepoFinding[];
@@ -23,8 +23,11 @@
23
23
  // 6. Policy (phase 6) — element marked Active/Production with no
24
24
  // owner.
25
25
  //
26
- // Findings stay `{ scope, id, message }` — no severity, no target/category
27
- // taxonomy (deferred per the ADR).
26
+ // Findings stay `{ scope, id, message }` plus the two un-frozen optional
27
+ // fields `ruleId` and `severity` — the richer target/category taxonomy stays
28
+ // deferred per the ADR.
29
+ import { checkStrategyChainSemantics } from './check-strategy-chain.js';
30
+ import { checkElementHygiene } from './check-element-hygiene.js';
28
31
  const PScope = 'repo';
29
32
  /** Statuses that, per lint.py's policy check, require an owner. */
30
33
  const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
@@ -330,6 +333,7 @@ function checkLayerSemantics(input, findings) {
330
333
  findings.push({
331
334
  scope: PScope,
332
335
  id: svcId,
336
+ ruleId: 'TSVC-003',
333
337
  message: `TSVC-003: TECHNOLOGY_SERVICE '${svcId}' node '${nodeRef}' ` +
334
338
  `must resolve to a NODE element; got notation='${resolved['notation']}'.`,
335
339
  });
@@ -354,6 +358,7 @@ function checkLayerSemantics(input, findings) {
354
358
  findings.push({
355
359
  scope: PScope,
356
360
  id: intId,
361
+ ruleId: 'INT-002',
357
362
  message: `INT-002: INTEGRATION '${intId}' interface_semantics endpoint '${endpointId}' ` +
358
363
  `(${field}) must resolve to an APPLICATION element; got notation='${resolved['notation']}'.`,
359
364
  });
@@ -384,8 +389,9 @@ function checkPolicy(input, findings) {
384
389
  /**
385
390
  * Run all repo-scope checks over a loaded canon model and return the findings.
386
391
  * Pure: no IO, deterministic order (syntax, uniqueness, atomicity, referential,
387
- * semantics, policy). Reaches parity with `lint.py` on the `acme_corp` fixture
388
- * (zero findings on a clean tree).
392
+ * semantics, policy, strategy-chain). Reaches parity with `lint.py` on the
393
+ * `acme_corp` fixture (zero findings on a clean tree from the original
394
+ * lint.py-ported phases).
389
395
  */
390
396
  export function validateRepoModel(input) {
391
397
  const findings = [];
@@ -400,5 +406,11 @@ export function validateRepoModel(input) {
400
406
  checkReferentialIntegrity(input, findings);
401
407
  checkLayerSemantics(input, findings);
402
408
  checkPolicy(input, findings);
409
+ // Phase 7 — strategy-chain semantic rules ported from DSM's Go Validate*
410
+ // functions (GOALS-010, ACT-006..009, FGCA-008..011).
411
+ checkStrategyChainSemantics(input, findings);
412
+ // Phase 8 — standalone-element envelope rules ported from DSM's Go
413
+ // Validate*Element functions (GOAL-ELEM-001..003, ACTION-001/002/005).
414
+ checkElementHygiene(input, findings);
403
415
  return findings;
404
416
  }
@@ -10,6 +10,15 @@ import { type NodeSizePreset } from '../node-size-presets.js';
10
10
  export declare const ACTIVITIES_NETWORK_CSS = "\n .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }\n .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1; }\n .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }\n .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1.5; }\n .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }\n";
11
11
  /** Arrowhead marker defs shared by both hosts for the network view. */
12
12
  export declare const ACTIVITIES_NETWORK_DEFS = "<defs>\n <marker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill\"/>\n </marker>\n <marker id=\"arrow-crit\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill-critical\"/>\n </marker>\n</defs>";
13
+ /**
14
+ * Extra top padding (px) a network-view canvas must add so skip-edge bows
15
+ * (see `blockingNodes`/`bowY` above) never draw above the SVG's y=0 and get
16
+ * clipped. `oyBase` is the canvas's Y offset *before* this padding — the
17
+ * historical `-bounds.y + N_PAD + titleH` both hosts compute — since a bow
18
+ * near the topmost row can reach above the diagram's own node bounds.
19
+ * Callers add the returned value to both `oyBase` and the canvas height.
20
+ */
21
+ export declare function computeNetworkTopPad(layout: ActivitiesLayout, oyBase: number): number;
13
22
  /**
14
23
  * Canonical Network (PSND) body — the node rects and edge paths that go inside
15
24
  * the `<svg>`, shared verbatim by the VS Code preview and the host-neutral