@transitrix/diagrams 1.9.18 → 1.11.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.
Files changed (87) hide show
  1. package/dist/activities/resolver.d.ts +3 -0
  2. package/dist/activities/resolver.js +39 -18
  3. package/dist/activities/validate.js +59 -0
  4. package/dist/canon-resolver-utils.d.ts +3 -0
  5. package/dist/canon-resolver-utils.js +16 -0
  6. package/dist/codex/index.d.ts +1 -1
  7. package/dist/codex/index.js +1 -1
  8. package/dist/codex/types.d.ts +9 -2
  9. package/dist/codex/types.js +10 -3
  10. package/dist/codex/validate.js +29 -3
  11. package/dist/compliance/classify.d.ts +11 -1
  12. package/dist/compliance/classify.js +31 -17
  13. package/dist/compliance/impact.d.ts +3 -1
  14. package/dist/compliance/impact.js +41 -13
  15. package/dist/compliance/types.d.ts +6 -0
  16. package/dist/compliance-matrix/build.js +0 -0
  17. package/dist/compliance-verification-matrix/build.d.ts +17 -0
  18. package/dist/compliance-verification-matrix/build.js +65 -0
  19. package/dist/compliance-verification-matrix/csv.d.ts +6 -0
  20. package/dist/compliance-verification-matrix/csv.js +41 -0
  21. package/dist/compliance-verification-matrix/index.d.ts +3 -0
  22. package/dist/compliance-verification-matrix/index.js +2 -0
  23. package/dist/compliance-verification-matrix/types.d.ts +75 -0
  24. package/dist/compliance-verification-matrix/types.js +4 -0
  25. package/dist/fgca/action-change-ids.d.ts +7 -0
  26. package/dist/fgca/action-change-ids.js +11 -0
  27. package/dist/fgca/parse-canonical.d.ts +3 -1
  28. package/dist/fgca/parse-canonical.js +13 -8
  29. package/dist/fgca/resolver.d.ts +6 -2
  30. package/dist/fgca/resolver.js +12 -6
  31. package/dist/fgca/validate.d.ts +2 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/process-blueprint/index.d.ts +3 -1
  35. package/dist/process-blueprint/index.js +1 -0
  36. package/dist/process-blueprint/layout.js +33 -29
  37. package/dist/process-blueprint/resolve-columns.d.ts +52 -0
  38. package/dist/process-blueprint/resolve-columns.js +138 -0
  39. package/dist/process-blueprint/types.d.ts +23 -5
  40. package/dist/process-blueprint/validate.d.ts +19 -1
  41. package/dist/process-blueprint/validate.js +73 -21
  42. package/dist/repo-validate/check-candidate-fields.d.ts +6 -0
  43. package/dist/repo-validate/check-candidate-fields.js +87 -0
  44. package/dist/repo-validate/check-process-parent.d.ts +8 -0
  45. package/dist/repo-validate/check-process-parent.js +172 -0
  46. package/dist/repo-validate/index.d.ts +1 -0
  47. package/dist/repo-validate/index.js +1 -0
  48. package/dist/repo-validate/validate-repo.js +7 -0
  49. package/dist/requirement/types.d.ts +1 -1
  50. package/dist/requirement/types.js +1 -1
  51. package/dist/webview/entry.js +11 -4
  52. package/dist/webview/render-fgca.js +1 -1
  53. package/dist/webview/render-process.d.ts +19 -1
  54. package/dist/webview/render-process.js +75 -42
  55. package/package.json +1 -1
  56. package/src/activities/resolver.ts +40 -20
  57. package/src/activities/validate.ts +58 -0
  58. package/src/canon-resolver-utils.ts +19 -0
  59. package/src/codex/index.ts +1 -1
  60. package/src/codex/types.ts +11 -3
  61. package/src/codex/validate.ts +30 -2
  62. package/src/compliance/classify.ts +40 -17
  63. package/src/compliance/impact.ts +41 -12
  64. package/src/compliance/types.ts +6 -0
  65. package/src/compliance-matrix/build.ts +0 -0
  66. package/src/compliance-verification-matrix/build.ts +84 -0
  67. package/src/compliance-verification-matrix/csv.ts +47 -0
  68. package/src/compliance-verification-matrix/index.ts +7 -0
  69. package/src/compliance-verification-matrix/types.ts +85 -0
  70. package/src/fgca/action-change-ids.ts +13 -0
  71. package/src/fgca/parse-canonical.ts +12 -8
  72. package/src/fgca/resolver.ts +15 -6
  73. package/src/fgca/validate.ts +2 -0
  74. package/src/index.ts +1 -0
  75. package/src/process-blueprint/index.ts +11 -0
  76. package/src/process-blueprint/layout.ts +48 -29
  77. package/src/process-blueprint/resolve-columns.ts +147 -0
  78. package/src/process-blueprint/types.ts +19 -5
  79. package/src/process-blueprint/validate.ts +107 -21
  80. package/src/repo-validate/check-candidate-fields.ts +93 -0
  81. package/src/repo-validate/check-process-parent.ts +184 -0
  82. package/src/repo-validate/index.ts +5 -0
  83. package/src/repo-validate/validate-repo.ts +7 -0
  84. package/src/requirement/types.ts +1 -1
  85. package/src/webview/entry.ts +13 -6
  86. package/src/webview/render-fgca.ts +1 -1
  87. package/src/webview/render-process.ts +94 -49
@@ -7,7 +7,7 @@ export interface GateChecks {
7
7
  completeness?: string;
8
8
  }
9
9
  /** TYPEs a requirement may be derived from (15-requirement.md §2, REQ-003). */
10
- export declare const REQUIREMENT_DERIVED_FROM_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD"];
10
+ export declare const REQUIREMENT_DERIVED_FROM_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD", "PRINCIPLE"];
11
11
  /** ISO/IEC/IEEE 29148 specification-tier ladder (15-requirement.md §2.5, REQ-005). */
12
12
  export type RequirementLevel = 'stakeholder' | 'system' | 'software';
13
13
  export declare const REQUIREMENT_LEVELS: readonly RequirementLevel[];
@@ -1,6 +1,6 @@
1
1
  // REQUIREMENT — motivation-layer positive obligation.
2
2
  // Schema: methodology notations/elements/15-requirement.md §2.
3
3
  /** TYPEs a requirement may be derived from (15-requirement.md §2, REQ-003). */
4
- export const REQUIREMENT_DERIVED_FROM_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'];
4
+ export const REQUIREMENT_DERIVED_FROM_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'];
5
5
  export const REQUIREMENT_LEVELS = ['stakeholder', 'system', 'software'];
6
6
  export const REQUIREMENT_KINDS = ['functional', 'quality'];
@@ -28,7 +28,7 @@ import yaml from 'js-yaml';
28
28
  import { validateActivities } from '../activities/index.js';
29
29
  import { validateActivityCard } from '../activity-card/index.js';
30
30
  import { validateApplicationsCatalogue } from '../applications/index.js';
31
- import { validateNestedBlocks } from '../blocks/index.js';
31
+ import { validateBlocks } from '../blocks/index.js';
32
32
  import { validateCapabilityMap } from '../capability-map/validate.js';
33
33
  import { parseCanonicalFGCA, parseCanonicalFGA } from '../fgca/parse-canonical.js';
34
34
  import { parseCanonicalGoals } from '../goals/parse-canonical.js';
@@ -40,7 +40,7 @@ import { coerceDatesToIsoStrings } from '../yaml-normalize.js';
40
40
  import { renderActivitiesSvg } from './render-activities.js';
41
41
  import { renderActivityCardSvg } from './render-activity-card.js';
42
42
  import { renderApplicationsHtml } from './render-applications.js';
43
- import { renderBlocksSvg } from './render-blocks.js';
43
+ import { renderBlocksSvg, renderGridSvg } from './render-blocks.js';
44
44
  import { renderCapabilityMapHtml } from './render-capability-map.js';
45
45
  import { renderFgcaSvg } from './render-fgca.js';
46
46
  import { renderGoalsSvg } from './render-goals.js';
@@ -138,8 +138,15 @@ function dispatchValidate(kind, doc) {
138
138
  return renderFromValidation('action-card', validateActivityCard(doc), () => renderActivityCardSvg(doc));
139
139
  case 'process-blueprint':
140
140
  return renderFromValidation('process-blueprint', validateProcessBlueprint(doc), () => renderProcessBlueprintSvg(doc));
141
- case 'blocks':
142
- return renderFromValidation('blocks', validateNestedBlocks(doc), () => renderBlocksSvg(doc));
141
+ case 'blocks': {
142
+ // Two alternative root forms (08-blocks.md §4): a `nested_blocks:`
143
+ // containment tree, or a `grid:` matrix subset (§4a). `validateBlocks`
144
+ // is the shared root-form dispatcher (BL-020 on both/neither present);
145
+ // mirrors the VS Code path's `hasGrid` branch in `blocks-preview.ts`.
146
+ const raw = (doc && typeof doc === 'object' ? doc : {});
147
+ const hasGrid = raw['grid'] !== undefined && raw['grid'] !== null;
148
+ return renderFromValidation('blocks', validateBlocks(doc), () => hasGrid ? renderGridSvg(doc) : renderBlocksSvg(doc));
149
+ }
143
150
  // Catalogue notations render an HTML fragment from the header nested under
144
151
  // their top-level wrapper key (`<thing>_catalogue` / `scenario` / …).
145
152
  case 'applications':
@@ -67,7 +67,7 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
67
67
  }
68
68
  export function renderFgcaSvg(doc, options = {}) {
69
69
  const { variant = 'dgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, nodeSizePreset = 'normal', layoutOptions, } = options;
70
- const hideChanges = variant === 'dga';
70
+ const hideChanges = variant === 'dga' || doc.hideChanges === true;
71
71
  const nodeSize = resolveDgcaNodeSize(parseNodeSizePreset(nodeSizePreset));
72
72
  const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, {
73
73
  hideChanges,
@@ -64,13 +64,31 @@ export interface ProcessDiagramLayout {
64
64
  flows: ProcessFlow[];
65
65
  associations: ProcessAssociation[];
66
66
  }
67
+ export interface BpmnTypography {
68
+ taskFontPx: number;
69
+ headerFontPx: number;
70
+ labelFontPx: number;
71
+ taskChars: number;
72
+ taskLineH: number;
73
+ labelChars: number;
74
+ labelLineH: number;
75
+ headerCharW: number;
76
+ }
77
+ /** Interactive-preview / default-export type sizes. */
78
+ export declare const DEFAULT_BPMN_TYPOGRAPHY: BpmnTypography;
79
+ /** Presentation-export type sizes (20 px floor, wrap tuned to default node boxes). */
80
+ export declare const PRESENTATION_BPMN_TYPOGRAPHY: BpmnTypography;
67
81
  export interface RenderProcessOptions {
68
82
  /** Raw SVG injected after `<style>` — a title line or full title block. */
69
83
  title?: string;
70
84
  /** Extra vertical space (px) reserved above the pool for a title block. */
71
85
  topInset?: number;
86
+ /** Label metrics. Omit for the interactive-preview defaults. */
87
+ typography?: BpmnTypography;
72
88
  }
73
- export declare const BPMN_PROCESS_CSS = "\n.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }\n.bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 11px; font-weight: 700; }\n.bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 11px; font-weight: 600; }\n.bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 11px; text-anchor: middle; }\n.bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }\n.bpmn-event-start { stroke-width: 1.5; }\n.bpmn-event-end { stroke-width: 4; }\n.bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 10px; text-anchor: middle; }\n.bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }\n.bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 10px; text-anchor: middle; }\n.bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }\n.bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }\n.bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }\n.bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }\n.bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }\n.bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }\n.bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }\n";
89
+ export declare function bpmnProcessCss(t?: BpmnTypography): string;
90
+ /** Default-preview CSS (11 px / 10 px). Presentation export builds its own via `bpmnProcessCss`. */
91
+ export declare const BPMN_PROCESS_CSS: string;
74
92
  /**
75
93
  * Canonical body emitter, shared by every host (VS Code, JCEF, static export).
76
94
  *
@@ -14,6 +14,28 @@
14
14
  */
15
15
  import { generateSvgEmbedCss } from '../theme/index.js';
16
16
  import { escXml } from './render-util.js';
17
+ /** Interactive-preview / default-export type sizes. */
18
+ export const DEFAULT_BPMN_TYPOGRAPHY = {
19
+ taskFontPx: 11,
20
+ headerFontPx: 11,
21
+ labelFontPx: 10,
22
+ taskChars: 14,
23
+ taskLineH: 14,
24
+ labelChars: 18,
25
+ labelLineH: 13,
26
+ headerCharW: 6.5,
27
+ };
28
+ /** Presentation-export type sizes (20 px floor, wrap tuned to default node boxes). */
29
+ export const PRESENTATION_BPMN_TYPOGRAPHY = {
30
+ taskFontPx: 20,
31
+ headerFontPx: 20,
32
+ labelFontPx: 20,
33
+ taskChars: 9,
34
+ taskLineH: 22,
35
+ labelChars: 14,
36
+ labelLineH: 22,
37
+ headerCharW: 11.5,
38
+ };
17
39
  // ---------------------------------------------------------------------------
18
40
  // Constants — match DEFAULT_LAYOUT_DIAGRAM_OPTIONS in src/layout-options.ts
19
41
  // ---------------------------------------------------------------------------
@@ -21,43 +43,49 @@ import { escXml } from './render-util.js';
21
43
  const LANE_LABEL_BAND = 44;
22
44
  /** End margin (px) along the header height axis for rotated pool/lane captions. */
23
45
  const HEADER_LABEL_AXIS_PAD = 32;
24
- /** Estimated horizontal advance (px) per character at the header label font size. */
25
- const HEADER_LABEL_CHAR_W = 6.5;
26
- /** Maximum characters per wrapped line inside a task box. */
27
- const TASK_CHARS = 14;
28
- /** Line height for wrapped task-name text (px). */
29
- const TASK_LINE_H = 14;
30
- /** Maximum characters per line for below-element labels (events, gateways). */
31
- const LABEL_CHARS = 18;
32
- /** Line height for multi-line below-element labels (px). */
33
- const LABEL_LINE_H = 13;
46
+ /** Active wrap/font metrics for the current render call (sync, restored after). */
47
+ let typo = DEFAULT_BPMN_TYPOGRAPHY;
48
+ function runWithTypography(t, fn) {
49
+ const prev = typo;
50
+ typo = t;
51
+ try {
52
+ return fn();
53
+ }
54
+ finally {
55
+ typo = prev;
56
+ }
57
+ }
34
58
  // ---------------------------------------------------------------------------
35
59
  // BPMN-specific CSS — no font-style:italic per CLAUDE.md design rule
36
60
  // ---------------------------------------------------------------------------
37
- export const BPMN_PROCESS_CSS = `
61
+ export function bpmnProcessCss(t = DEFAULT_BPMN_TYPOGRAPHY) {
62
+ return `
38
63
  .bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
39
64
  .bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
40
65
  .bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
41
66
  .bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
42
- .bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 11px; font-weight: 700; }
43
- .bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 11px; font-weight: 600; }
67
+ .bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.headerFontPx}px; font-weight: 700; }
68
+ .bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.headerFontPx}px; font-weight: 600; }
44
69
  .bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
45
- .bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 11px; text-anchor: middle; }
70
+ .bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.taskFontPx}px; text-anchor: middle; }
46
71
  .bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }
47
72
  .bpmn-event-start { stroke-width: 1.5; }
48
73
  .bpmn-event-end { stroke-width: 4; }
49
- .bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 10px; text-anchor: middle; }
74
+ .bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.labelFontPx}px; text-anchor: middle; }
50
75
  .bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
51
76
  .bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }
52
- .bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 10px; text-anchor: middle; }
77
+ .bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.labelFontPx}px; text-anchor: middle; }
53
78
  .bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }
54
79
  .bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }
55
80
  .bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }
56
81
  .bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
57
- .bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: 10px; text-anchor: middle; }
82
+ .bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: ${t.labelFontPx}px; text-anchor: middle; }
58
83
  .bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
59
84
  .bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
60
85
  `;
86
+ }
87
+ /** Default-preview CSS (11 px / 10 px). Presentation export builds its own via `bpmnProcessCss`. */
88
+ export const BPMN_PROCESS_CSS = bpmnProcessCss(DEFAULT_BPMN_TYPOGRAPHY);
61
89
  // ---------------------------------------------------------------------------
62
90
  // Internal helpers
63
91
  // ---------------------------------------------------------------------------
@@ -73,28 +101,29 @@ function wordTruncate(text, maxChars) {
73
101
  return (lastSpace > maxChars / 2 ? cut.slice(0, lastSpace) : cut) + '…';
74
102
  }
75
103
  function fitRotatedHeaderText(name, spanPx) {
76
- const maxChars = Math.max(4, Math.floor(spanPx / HEADER_LABEL_CHAR_W));
104
+ const maxChars = Math.max(4, Math.floor(spanPx / typo.headerCharW));
77
105
  return wordTruncate(name, maxChars);
78
106
  }
79
107
  function wrapTaskName(name) {
80
108
  if (!name)
81
109
  return [];
110
+ const limit = typo.taskChars;
82
111
  const words = name.split(/\s+/);
83
112
  const lines = [];
84
113
  let cur = '';
85
114
  for (const w of words) {
86
- const token = w.length > TASK_CHARS ? w.slice(0, TASK_CHARS - 1) + '…' : w;
115
+ const token = w.length > limit ? w.slice(0, limit - 1) + '…' : w;
87
116
  if (!cur) {
88
117
  cur = token;
89
118
  }
90
- else if (cur.length + 1 + token.length <= TASK_CHARS) {
119
+ else if (cur.length + 1 + token.length <= limit) {
91
120
  cur += ' ' + token;
92
121
  }
93
122
  else {
94
123
  lines.push(cur);
95
124
  if (lines.length === 3) {
96
- if (lines[2].length > TASK_CHARS - 1)
97
- lines[2] = lines[2].slice(0, TASK_CHARS - 1) + '…';
125
+ if (lines[2].length > limit - 1)
126
+ lines[2] = lines[2].slice(0, limit - 1) + '…';
98
127
  return lines;
99
128
  }
100
129
  cur = token;
@@ -103,28 +132,29 @@ function wrapTaskName(name) {
103
132
  if (cur && lines.length < 3)
104
133
  lines.push(cur);
105
134
  if (lines.length === 0)
106
- lines.push(name.slice(0, TASK_CHARS));
135
+ lines.push(name.slice(0, limit));
107
136
  return lines;
108
137
  }
109
138
  function taskNameSvg(name, cx, cy) {
110
139
  if (!name)
111
140
  return '';
112
141
  const lines = wrapTaskName(name);
113
- const totalH = lines.length * TASK_LINE_H;
114
- const startY = cy - totalH / 2 + TASK_LINE_H * 0.8;
142
+ const totalH = lines.length * typo.taskLineH;
143
+ const startY = cy - totalH / 2 + typo.taskLineH * 0.8;
115
144
  const tspans = lines
116
- .map((ln, i) => `<tspan x="${r(cx)}" y="${r(startY + i * TASK_LINE_H)}">${escXml(ln)}</tspan>`)
145
+ .map((ln, i) => `<tspan x="${r(cx)}" y="${r(startY + i * typo.taskLineH)}">${escXml(ln)}</tspan>`)
117
146
  .join('');
118
147
  return `<text class="bpmn-task-name">${tspans}</text>`;
119
148
  }
120
149
  function wrapBelowLabel(name) {
150
+ const limit = typo.labelChars;
121
151
  const words = name.split(/\s+/).filter(Boolean);
122
152
  const allLines = [];
123
153
  let cur = '';
124
154
  for (const word of words) {
125
- const tok = word.length > LABEL_CHARS ? word.slice(0, LABEL_CHARS - 1) + '…' : word;
155
+ const tok = word.length > limit ? word.slice(0, limit - 1) + '…' : word;
126
156
  const test = cur ? `${cur} ${tok}` : tok;
127
- if (test.length <= LABEL_CHARS) {
157
+ if (test.length <= limit) {
128
158
  cur = test;
129
159
  }
130
160
  else {
@@ -136,12 +166,12 @@ function wrapBelowLabel(name) {
136
166
  if (cur)
137
167
  allLines.push(cur);
138
168
  if (allLines.length === 0)
139
- return [name.slice(0, LABEL_CHARS)];
169
+ return [name.slice(0, limit)];
140
170
  if (allLines.length <= 3)
141
171
  return allLines;
142
172
  const lines = allLines.slice(0, 3);
143
173
  const last = lines[2];
144
- lines[2] = last.length + 1 <= LABEL_CHARS ? last + '…' : last.slice(0, LABEL_CHARS - 1) + '…';
174
+ lines[2] = last.length + 1 <= limit ? last + '…' : last.slice(0, limit - 1) + '…';
145
175
  return lines;
146
176
  }
147
177
  function belowLabelSvg(name, cls, cx, belowY, minX = 0) {
@@ -155,7 +185,7 @@ function belowLabelSvg(name, cls, cx, belowY, minX = 0) {
155
185
  return `<text class="${cls}" x="${r(textX)}" y="${startY}">${escXml(lines[0])}</text>`;
156
186
  }
157
187
  const tspans = lines
158
- .map((ln, i) => `<tspan x="${r(textX)}" dy="${i === 0 ? 0 : LABEL_LINE_H}">${escXml(ln)}</tspan>`)
188
+ .map((ln, i) => `<tspan x="${r(textX)}" dy="${i === 0 ? 0 : typo.labelLineH}">${escXml(ln)}</tspan>`)
159
189
  .join('');
160
190
  return `<text class="${cls}" x="${r(textX)}" y="${startY}">${tspans}</text>`;
161
191
  }
@@ -378,15 +408,18 @@ export function renderProcessBody(layout, ox, oy) {
378
408
  */
379
409
  export function renderProcessLayoutSvg(layout, options = {}) {
380
410
  const { title = '', topInset = title ? 32 : 0 } = options;
381
- const pb = layout.poolBounds;
382
- const PAD = 16;
383
- const svgW = r(pb.x + pb.width + PAD);
384
- const svgH = r(pb.y + pb.height + PAD + topInset);
385
- const embedCss = generateSvgEmbedCss('transitrix') + BPMN_PROCESS_CSS;
386
- const body = renderProcessBody(layout, 0, topInset);
387
- return (`<svg xmlns="http://www.w3.org/2000/svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">\n` +
388
- `<style>${embedCss}</style>\n` +
389
- (title ? `${title}\n` : '') +
390
- `${body}\n` +
391
- `</svg>`);
411
+ const t = options.typography ?? DEFAULT_BPMN_TYPOGRAPHY;
412
+ return runWithTypography(t, () => {
413
+ const pb = layout.poolBounds;
414
+ const PAD = 16;
415
+ const svgW = r(pb.x + pb.width + PAD);
416
+ const svgH = r(pb.y + pb.height + PAD + topInset);
417
+ const embedCss = generateSvgEmbedCss('transitrix') + bpmnProcessCss(t);
418
+ const body = renderProcessBody(layout, 0, topInset);
419
+ return (`<svg xmlns="http://www.w3.org/2000/svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">\n` +
420
+ `<style>${embedCss}</style>\n` +
421
+ (title ? `${title}\n` : '') +
422
+ `${body}\n` +
423
+ `</svg>`);
424
+ });
392
425
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.9.18",
3
+ "version": "1.11.0",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@
12
12
  * §2). Filesystem-free — callable from unit tests without vscode.
13
13
  */
14
14
 
15
- import { isObject, str, strArray, descendantsOf, stripEnvelope } from '../canon-resolver-utils.js';
15
+ import { isObject, str, strArray, stripEnvelope, reachesRootViaParent } from '../canon-resolver-utils.js';
16
16
 
17
17
  export interface ActionViewConfig {
18
18
  scope?: {
@@ -44,6 +44,10 @@ export interface ActionCanonSources {
44
44
  relations?: unknown[];
45
45
  }
46
46
 
47
+ /** In-memory bag of ACTION ids dropped by `root_action` scoping. Consumed by
48
+ * `validateActivities` as `ACT-021`. Not part of the notation. */
49
+ export const ACT_021_OMITTED_KEY = '__act021_omitted';
50
+
47
51
  /** ACTION elements by id. `notation: action` is canonical; `notation: activity`
48
52
  * is the deprecated pre-2026-06-25 alias (elements/24-action.md §"Deprecated
49
53
  * alias") — accepted the same way `collectByNotation` callers elsewhere do. */
@@ -123,32 +127,26 @@ export function resolveAction(
123
127
  return relGoals.length > 0 ? relGoals : strArray(el['goals']);
124
128
  }
125
129
 
126
- // 1. Scope by root_action (this action + its descendants), or the full
127
- // catalogue when omitted (§5.1: "Omit to include elements from the full
128
- // catalogue (or use goals/type_filter for other scoping)").
129
- const rootAction = str(scope['root_action']);
130
- let candidateIds: Set<string>;
131
- if (rootAction) {
132
- candidateIds = allActions.has(rootAction) ? descendantsOf(rootAction, allActions) : new Set();
133
- } else {
134
- candidateIds = new Set(allActions.keys());
135
- }
130
+ // 1. Other scope fields first — the set the view would otherwise select
131
+ // (07-action.md §5.1). `root_action` then omits (and warns) rather than
132
+ // silently intersecting first.
133
+ let otherwiseIds: Set<string> = new Set(allActions.keys());
136
134
 
137
- // 2. Narrow by goals — only actions linked to at least one listed GOAL
135
+ // Narrow by goals — only actions linked to at least one listed GOAL
138
136
  // (via an active action_goal relation, or inline goals[] when no relation
139
137
  // exists for that action).
140
138
  const goalsFilter = strArray(scope['goals']);
141
139
  if (goalsFilter.length > 0) {
142
- candidateIds = new Set(
143
- [...candidateIds].filter((id) => effectiveGoals(id, allActions.get(id)!).some((g) => goalsFilter.includes(g))),
140
+ otherwiseIds = new Set(
141
+ [...otherwiseIds].filter((id) => effectiveGoals(id, allActions.get(id)!).some((g) => goalsFilter.includes(g))),
144
142
  );
145
143
  }
146
144
 
147
- // 3. Narrow by type_filter — only actions of a listed type.
145
+ // Narrow by type_filter — only actions of a listed type.
148
146
  const typeFilter = strArray(scope['type_filter']);
149
147
  if (typeFilter.length > 0) {
150
- candidateIds = new Set(
151
- [...candidateIds].filter((id) => {
148
+ otherwiseIds = new Set(
149
+ [...otherwiseIds].filter((id) => {
152
150
  const el = allActions.get(id);
153
151
  const t = str(el?.['type']) ?? str(el?.['activity_type']);
154
152
  return t !== undefined && typeFilter.includes(t);
@@ -156,13 +154,13 @@ export function resolveAction(
156
154
  );
157
155
  }
158
156
 
159
- // 4. Narrow by valid_at — only actions in effect on that date. Actions with
157
+ // Narrow by valid_at — only actions in effect on that date. Actions with
160
158
  // no valid_from are not lifecycle-tracked and are kept (permissive default,
161
159
  // matching the resolver's general "missing = not excluded" stance).
162
160
  const validAt = str(scope['valid_at']);
163
161
  if (validAt) {
164
- candidateIds = new Set(
165
- [...candidateIds].filter((id) => {
162
+ otherwiseIds = new Set(
163
+ [...otherwiseIds].filter((id) => {
166
164
  const el = allActions.get(id);
167
165
  const from = str(el?.['valid_from']);
168
166
  const to = str(el?.['valid_to']);
@@ -173,6 +171,27 @@ export function resolveAction(
173
171
  );
174
172
  }
175
173
 
174
+ const rootAction = str(scope['root_action']);
175
+ let candidateIds: Set<string>;
176
+ const omittedOutsideRoot: string[] = [];
177
+ if (rootAction) {
178
+ if (!allActions.has(rootAction)) {
179
+ candidateIds = new Set();
180
+ } else {
181
+ candidateIds = new Set();
182
+ for (const id of otherwiseIds) {
183
+ if (reachesRootViaParent(id, rootAction, allActions)) {
184
+ candidateIds.add(id);
185
+ } else {
186
+ omittedOutsideRoot.push(id);
187
+ }
188
+ }
189
+ omittedOutsideRoot.sort();
190
+ }
191
+ } else {
192
+ candidateIds = otherwiseIds;
193
+ }
194
+
176
195
  // Element fields map onto Activity fields unchanged, except the canonical
177
196
  // `type` (elements/24-action.md §2) which becomes the internal `activity_type`
178
197
  // (types.ts `Activity.activity_type`), and `goals` which is resolved via
@@ -204,5 +223,6 @@ export function resolveAction(
204
223
  actions: selectedActions,
205
224
  };
206
225
  if (Object.keys(project).length > 0) out['project'] = project;
226
+ if (omittedOutsideRoot.length > 0) out[ACT_021_OMITTED_KEY] = { root: rootAction, ids: omittedOutsideRoot };
207
227
  return out;
208
228
  }
@@ -5,10 +5,62 @@ import type {
5
5
  ActivityValidationError,
6
6
  ActivityValidationWarning,
7
7
  } from './types.js';
8
+ import { isObject, str, reachesRootViaParent } from '../canon-resolver-utils.js';
9
+ import { ACT_021_OMITTED_KEY } from './resolver.js';
8
10
 
9
11
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
10
12
  const VALID_WEEKDAYS = new Set(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
11
13
 
14
+ function act021Message(actionId: string, rootId: string): string {
15
+ return `ACTION "${actionId}" is not "${rootId}" and is not reachable from it via parent`;
16
+ }
17
+
18
+ function collectAct021FromProjection(raw: Record<string, unknown>): ActivityValidationWarning[] {
19
+ const bag = raw[ACT_021_OMITTED_KEY];
20
+ delete raw[ACT_021_OMITTED_KEY];
21
+ if (!isObject(bag)) return [];
22
+ const root = str(bag['root']);
23
+ const ids = Array.isArray(bag['ids']) ? bag['ids'] : [];
24
+ if (!root) return [];
25
+ const out: ActivityValidationWarning[] = [];
26
+ for (const id of ids) {
27
+ if (typeof id !== 'string' || !id.trim()) continue;
28
+ out.push({
29
+ code: 'ACT-021',
30
+ message: act021Message(id, root),
31
+ path: 'view_config.scope.root_action',
32
+ });
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function collectAct021FromAuthored(
38
+ raw: Record<string, unknown>,
39
+ actions: unknown[],
40
+ ): ActivityValidationWarning[] {
41
+ const vc = isObject(raw['view_config']) ? raw['view_config'] : undefined;
42
+ const scope = vc && isObject(vc['scope']) ? vc['scope'] : undefined;
43
+ const root = scope ? str(scope['root_action']) : undefined;
44
+ if (!root) return [];
45
+ const byId = new Map<string, Record<string, unknown>>();
46
+ for (const el of actions) {
47
+ if (!isObject(el)) continue;
48
+ const id = str(el['id']);
49
+ if (id) byId.set(id, el);
50
+ }
51
+ if (!byId.has(root)) return [];
52
+ const out: ActivityValidationWarning[] = [];
53
+ for (const id of byId.keys()) {
54
+ if (reachesRootViaParent(id, root, byId)) continue;
55
+ out.push({
56
+ code: 'ACT-021',
57
+ message: act021Message(id, root),
58
+ path: 'view_config.scope.root_action',
59
+ });
60
+ }
61
+ return out;
62
+ }
63
+
12
64
  export function validateActivities(input: unknown): ActivityValidationResult {
13
65
  const errors: ActivityValidationError[] = [];
14
66
  const warnings: ActivityValidationWarning[] = [];
@@ -46,6 +98,12 @@ export function validateActivities(input: unknown): ActivityValidationResult {
46
98
  // Normalise onto raw.activities for the rest of this function (avoids touching downstream code)
47
99
  if (!Array.isArray(raw.activities)) raw.activities = raw.actions;
48
100
 
101
+ const fromProjection = collectAct021FromProjection(raw);
102
+ warnings.push(...fromProjection);
103
+ if (fromProjection.length === 0) {
104
+ warnings.push(...collectAct021FromAuthored(raw, rawArray));
105
+ }
106
+
49
107
  // ACT-014 / ACT-015: project.calendar validation (run before per-activity loop;
50
108
  // surfaces clearly even when activities also have issues).
51
109
  if (raw.project && typeof raw.project === 'object') {
@@ -48,6 +48,25 @@ export function descendantsOf(
48
48
  return result;
49
49
  }
50
50
 
51
+ /** Walk `parent` from `id` toward its ancestors until `rootId` or the chain
52
+ * ends. Cycle-safe: a cycle is not a path to the root. */
53
+ export function reachesRootViaParent(
54
+ id: string,
55
+ rootId: string,
56
+ all: Map<string, Record<string, unknown>>,
57
+ ): boolean {
58
+ const seen = new Set<string>();
59
+ let current: string | undefined = id;
60
+ while (current) {
61
+ if (current === rootId) return true;
62
+ if (seen.has(current)) return false;
63
+ seen.add(current);
64
+ const el = all.get(current);
65
+ current = el ? str(el['parent']) : undefined;
66
+ }
67
+ return false;
68
+ }
69
+
51
70
  /** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
52
71
  * adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
53
72
  * memoized across calls via the shared `memo` map. */
@@ -1,4 +1,4 @@
1
- export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES } from './types.js';
1
+ export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, PRINCIPLE_CODEX_TYPES } from './types.js';
2
2
  export type { CodexArtefactType } from './types.js';
3
3
  export { validateCodex, isCodexDoc, folderJurisdictionFromPath } from './validate.js';
4
4
  export type { CodexValidateOptions } from './validate.js';
@@ -1,13 +1,21 @@
1
- // Codex artefact — external laws/regulations and internal policies/standards.
1
+ // Codex artefact — external laws/regulations and internal policies/standards/principles.
2
2
  // Schema: methodology notations/elements/14-codex.md §3–§4.
3
3
 
4
4
  /** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
5
- export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'] as const;
5
+ export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'] as const;
6
6
 
7
7
  export type CodexArtefactType = (typeof CODEX_ARTEFACT_TYPES)[number];
8
8
 
9
9
  /** External codex artefacts (codex/external/<jurisdiction>/). */
10
10
  export const EXTERNAL_CODEX_TYPES: readonly CodexArtefactType[] = ['LAW', 'REGULATION'];
11
11
 
12
- /** Internal codex artefacts (codex/internal/). */
12
+ /** Internal codex artefacts (codex/internal/) that carry a named issuing authority (§4). */
13
13
  export const INTERNAL_CODEX_TYPES: readonly CodexArtefactType[] = ['POLICY', 'INTERNAL_STANDARD'];
14
+
15
+ /**
16
+ * Internal codex artefacts (codex/internal/) with the `PRINCIPLE` shape (§4.1) —
17
+ * `statement` + `rationale` required, `issuing_authority`/`effective_date`/`established_by`
18
+ * optional. Kept distinct from {@link INTERNAL_CODEX_TYPES} because the required-field set
19
+ * differs; both share the same file location.
20
+ */
21
+ export const PRINCIPLE_CODEX_TYPES: readonly CodexArtefactType[] = ['PRINCIPLE'];
@@ -3,9 +3,20 @@
3
3
  // CODEX-001 — shape, zone, id grammar, admission envelope.
4
4
  // CODEX-002 — `type` field matches the id TYPE prefix when present.
5
5
  // CODEX-003 — external artefact frontmatter (jurisdiction, effective_date).
6
- // CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date).
6
+ // CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date for
7
+ // POLICY/INTERNAL_STANDARD; statement, rationale for PRINCIPLE — §4.1).
7
8
  // CODEX-005 — jurisdiction must match the parent folder under codex/external/
8
9
  // (when `folderJurisdiction` is supplied by the repo sweep).
10
+ // CODEX-006 — a PRINCIPLE artefact does not declare `established_by` (§4.1). The spec
11
+ // marks this "info" severity; this module has no info tier, so it is
12
+ // surfaced as a warning — the nearest existing severity that does not fail
13
+ // `valid`.
14
+ //
15
+ // Note: this file's CODEX-00x numbering predates the spec's own v0.2 renumbering
16
+ // (14-codex.md §6, §8.2) and does not track it code-for-code — e.g. the spec's current
17
+ // CODEX-001 is a folder/jurisdiction check this file assigns to CODEX-005. Realigning the
18
+ // full scheme is out of scope here; this change only adds the purely-additive PRINCIPLE
19
+ // TYPE (§4.1) under the existing local numbering.
9
20
 
10
21
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
11
22
  import { isCanonicalId, typeOfId } from '../typed-id.js';
@@ -13,6 +24,7 @@ import {
13
24
  CODEX_ARTEFACT_TYPES,
14
25
  EXTERNAL_CODEX_TYPES,
15
26
  INTERNAL_CODEX_TYPES,
27
+ PRINCIPLE_CODEX_TYPES,
16
28
  type CodexArtefactType,
17
29
  } from './types.js';
18
30
 
@@ -48,7 +60,7 @@ export function validateCodex(input: unknown, options: CodexValidateOptions = {}
48
60
  if (!isCanonicalId(c.id) || !isCodexType(idType)) {
49
61
  errors.push({
50
62
  code: 'CODEX-001',
51
- message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, or INTERNAL_STANDARD).`,
63
+ message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, INTERNAL_STANDARD, or PRINCIPLE).`,
52
64
  path: 'id',
53
65
  });
54
66
  }
@@ -106,6 +118,22 @@ export function validateCodex(input: unknown, options: CodexValidateOptions = {}
106
118
  }
107
119
  }
108
120
 
121
+ if (artefactType && (PRINCIPLE_CODEX_TYPES as readonly string[]).includes(artefactType)) {
122
+ if (typeof c.statement !== 'string' || c.statement.trim() === '') {
123
+ errors.push({ code: 'CODEX-004', message: 'statement is required for a PRINCIPLE artefact.', path: 'statement' });
124
+ }
125
+ if (typeof c.rationale !== 'string' || c.rationale.trim() === '') {
126
+ errors.push({ code: 'CODEX-004', message: 'rationale is required for a PRINCIPLE artefact.', path: 'rationale' });
127
+ }
128
+ if (typeof c.established_by !== 'string' || c.established_by.trim() === '') {
129
+ warnings.push({
130
+ code: 'CODEX-006',
131
+ message: 'established_by is not declared on this PRINCIPLE artefact — admissible, but a review finding for whoever admits it.',
132
+ path: 'established_by',
133
+ });
134
+ }
135
+ }
136
+
109
137
  return { valid: errors.length === 0, errors, warnings };
110
138
  }
111
139