From 4b0a7b252d175cae904f9a285ff5dbd90dea325d Mon Sep 17 00:00:00 2001 From: "Leo M." Date: Sat, 27 Jun 2026 20:36:07 -0600 Subject: [PATCH] fix(cynefin): align with Dave Snowden's framework corrections Folds in three accuracy corrections raised by Dave Snowden (the creator of the Cynefin framework) after reviewing the released diagram: - The central domain is the Aporetic domain (productive not-knowing), not "Confusion"/"Disorder". "Confused" only exists in the liminal model, which this diagram does not render. Renames the `confusion` keyword to `aporetic` and the `confusionBg` theme variable to `aporeticBg`, and drops the empty centre subtitle. - The Clear/Complicated boundary is a gradient, not a phase shift, so it is now drawn as a plain straight line, distinct from the wavy phase-shift boundaries (the central fold, the Complex/Chaotic boundary, and the Clear/Chaotic cliff). - Flow radiates outward from the Aporetic centre, not in from one side. Adds outward flow arrows from the centre to each domain behind a new `showFlow` config option (default true). BREAKING CHANGE: the `confusion` domain keyword is renamed to `aporetic`. Acceptable while the diagram is still `cynefin-beta`. Closes #7921 --- .cspell/mermaid-terms.txt | 1 + .../rendering/cynefin/cynefin.spec.js | 22 +++- docs/syntax/cynefin.md | 55 ++++----- packages/examples/src/examples/cynefin.ts | 2 +- packages/mermaid/src/config.type.ts | 7 ++ .../cynefin/cynefin.integration.spec.ts | 12 +- .../src/diagrams/cynefin/cynefin.spec.ts | 30 ++++- .../src/diagrams/cynefin/cynefinBoundaries.ts | 41 ++++++- .../src/diagrams/cynefin/cynefinRenderer.ts | 114 ++++++++++++------ .../mermaid/src/diagrams/cynefin/styles.ts | 19 ++- .../mermaid/src/diagrams/cynefin/types.ts | 2 +- packages/mermaid/src/docs/syntax/cynefin.md | 53 ++++---- .../mermaid/src/schemas/config.schema.yaml | 7 ++ packages/mermaid/src/themes/theme-base.js | 2 +- packages/mermaid/src/themes/theme-dark.js | 2 +- packages/mermaid/src/themes/theme-default.js | 2 +- packages/mermaid/src/themes/theme-forest.js | 2 +- packages/mermaid/src/themes/theme-neutral.js | 2 +- .../src/language/cynefin/cynefin.langium | 2 +- 19 files changed, 259 insertions(+), 118 deletions(-) diff --git a/.cspell/mermaid-terms.txt b/.cspell/mermaid-terms.txt index a0e57f51d82..86caf68892a 100644 --- a/.cspell/mermaid-terms.txt +++ b/.cspell/mermaid-terms.txt @@ -1,4 +1,5 @@ Adamiecki +aporetic arrowend Bendpoints bmatrix diff --git a/cypress/integration/rendering/cynefin/cynefin.spec.js b/cypress/integration/rendering/cynefin/cynefin.spec.js index d061bc8e84d..1781b705377 100644 --- a/cypress/integration/rendering/cynefin/cynefin.spec.js +++ b/cypress/integration/rendering/cynefin/cynefin.spec.js @@ -21,7 +21,7 @@ describe('cynefin framework', () => { chaotic "Page on-call immediately" - confusion + aporetic "Unknown failure mode" ` ); @@ -104,6 +104,20 @@ describe('cynefin framework', () => { ); }); + it('should render cynefin without outward flow arrows', () => { + imgSnapshotTest( + `cynefin-beta + title No Flow + + complex + "Item A" + clear + "Item B" + `, + { cynefin: { showFlow: false } } + ); + }); + it('should render cynefin without domain descriptions', () => { imgSnapshotTest( `cynefin-beta @@ -157,12 +171,12 @@ describe('cynefin framework', () => { ); }); - it('should render cynefin with confusion domain items without overflow', () => { + it('should render cynefin with aporetic domain items without overflow', () => { imgSnapshotTest( `cynefin-beta - title Confusion Items + title Aporetic Items - confusion + aporetic "Unknown A" "Unknown B" "Unknown C" diff --git a/docs/syntax/cynefin.md b/docs/syntax/cynefin.md index e9d15b1aafc..00a445bea13 100644 --- a/docs/syntax/cynefin.md +++ b/docs/syntax/cynefin.md @@ -18,9 +18,9 @@ The Cynefin framework divides the world into five domains, each with its own dec - **Complicated**: Cause and effect require analysis or expertise. Sense → Analyse → Respond. Apply **good practices**. - **Complex**: Cause and effect can only be deduced in retrospect. Probe → Sense → Respond. Apply **emergent practices**. - **Chaotic**: No perceivable cause and effect. Act → Sense → Respond. Apply **novel practices**. -- **Confusion** (or Disorder): You do not know which domain you are in. The goal is to move items out of this state into one of the other four. +- **Aporetic** (the central domain): You do not yet know which domain you are in — a state of productive not-knowing. The goal is to move items out of this state into one of the other four. (Note: "Confused" is a distinct concept that only appears in the liminal/dynamic version of Cynefin, which this diagram does not model.) -The signature visual feature is the wavy, organic boundary between the ordered (Clear, Complicated) and unordered (Complex, Chaotic) halves, and the "cliff" between Clear and Chaotic representing the risk of complacency leading to crisis. +The signature visual features are the phase-shift boundaries between domains — wavy and abrupt where crossing them is a real transition (the central fold, the Complex/Chaotic boundary, and the "cliff" between Clear and Chaotic) — versus the plain Clear/Complicated boundary, which is a gradient rather than a phase shift. Flow radiates outward from the central Aporetic domain. ## Syntax @@ -43,7 +43,7 @@ clear chaotic "Crisis response" -confusion +aporetic "Item of unknown domain" complex --> complicated : "Pattern identified" @@ -60,7 +60,7 @@ clear --> chaotic : "Complacency" | `complicated` | Opens the Complicated domain block | | `clear` | Opens the Clear domain block | | `chaotic` | Opens the Chaotic domain block | -| `confusion` | Opens the Confusion / Disorder domain block | +| `aporetic` | Opens the central Aporetic (not-yet-known) domain block | | `-->` | Declares a transition from one domain to another | ### Items @@ -73,7 +73,7 @@ complex "Run chaos experiment" ``` -Keep per-domain item lists short — the quadrants have fixed layout and long lists can visually overflow their boxes. The confusion ellipse caps at three items and shows a `+N more` badge; the four quadrant domains do not clip, so prefer a handful of items each. +Keep per-domain item lists short — the quadrants have fixed layout and long lists can visually overflow their boxes. The aporetic ellipse caps at three items and shows a `+N more` badge; the four quadrant domains do not clip, so prefer a handful of items each. ### Transitions @@ -115,7 +115,7 @@ cynefin-beta chaotic "Page on-call immediately" - confusion + aporetic "Unknown failure mode" ``` @@ -138,7 +138,7 @@ cynefin-beta chaotic "Page on-call immediately" - confusion + aporetic "Unknown failure mode" ``` @@ -224,6 +224,7 @@ Cynefin diagrams accept the following configuration under the `cynefin` key in t | `showDomainDescriptions` | boolean | `true` | Show decision model and practice type subtitles per domain | | `boundaryAmplitude` | number | `8` | Waviness amplitude of domain boundaries in pixels (set to `0` for straight lines) | | `seed` | number | `0` | Deterministic seed for boundary waviness. `0` (default) hashes the diagram's SVG id so each diagram looks unique. Set any non-zero number to lock the waviness across renders — required for stable visual regression tests. | +| `showFlow` | boolean | `true` | Show flow arrows radiating outward from the central Aporetic domain to each domain (set to `false` to hide them) | Example: @@ -238,29 +239,29 @@ cynefin-beta Cynefin diagrams use the following theme variables, which can be overridden via `themeVariables.cynefin`: -| Variable | Description | -| ---------------- | ------------------------------------------------ | -| `complexBg` | Background color for the Complex domain | -| `complicatedBg` | Background color for the Complicated domain | -| `clearBg` | Background color for the Clear domain | -| `chaoticBg` | Background color for the Chaotic domain | -| `confusionBg` | Background color for the Confusion center region | -| `boundaryColor` | Color of the wavy domain boundaries | -| `boundaryWidth` | Stroke width of the boundaries | -| `cliffColor` | Color of the Clear/Chaotic cliff | -| `cliffWidth` | Stroke width of the cliff | -| `arrowColor` | Color of transition arrows | -| `arrowWidth` | Stroke width of transition arrows | -| `labelColor` | Color of domain name labels | -| `textColor` | Color of item and subtitle text | -| `domainFontSize` | Font size of domain name labels | -| `itemFontSize` | Font size of item badges and subtitles | +| Variable | Description | +| ---------------- | ----------------------------------------------- | +| `complexBg` | Background color for the Complex domain | +| `complicatedBg` | Background color for the Complicated domain | +| `clearBg` | Background color for the Clear domain | +| `chaoticBg` | Background color for the Chaotic domain | +| `aporeticBg` | Background color for the Aporetic center region | +| `boundaryColor` | Color of the wavy domain boundaries | +| `boundaryWidth` | Stroke width of the boundaries | +| `cliffColor` | Color of the Clear/Chaotic cliff | +| `cliffWidth` | Stroke width of the cliff | +| `arrowColor` | Color of transition arrows | +| `arrowWidth` | Stroke width of transition arrows | +| `labelColor` | Color of domain name labels | +| `textColor` | Color of item and subtitle text | +| `domainFontSize` | Font size of domain name labels | +| `itemFontSize` | Font size of item badges and subtitles | ## Notes -- Domain names are fixed keywords. Only `complex`, `complicated`, `clear`, `chaotic`, and `confusion` are recognized. -- Domains can be declared in any order; their position in the diagram is always the same (Complex top-left, Complicated top-right, Chaotic bottom-left, Clear bottom-right, Confusion center). -- The `confusion` domain has a compact center ellipse. Up to 3 items are shown inside it; if more are provided a `+N more` overflow badge is displayed. In practice, the confusion domain should contain very few items — its purpose is to surface unknowns so they can be moved to one of the four main domains. +- Domain names are fixed keywords. Only `complex`, `complicated`, `clear`, `chaotic`, and `aporetic` are recognized. +- Domains can be declared in any order; their position in the diagram is always the same (Complex top-left, Complicated top-right, Chaotic bottom-left, Clear bottom-right, Aporetic center). +- The `aporetic` domain has a compact center ellipse. Up to 3 items are shown inside it; if more are provided a `+N more` overflow badge is displayed. In practice, the aporetic domain should contain very few items — its purpose is to surface unknowns so they can be moved to one of the four main domains. - Self-loop transitions (e.g. `complex --> complex`) are silently ignored. Transitions must connect two different domains. - Handdrawn mode is not currently supported. - The wavy boundary rendering is deterministic: the same input always produces the same diagram, so diffs are stable across builds. diff --git a/packages/examples/src/examples/cynefin.ts b/packages/examples/src/examples/cynefin.ts index 72abfb16894..0de589099ee 100644 --- a/packages/examples/src/examples/cynefin.ts +++ b/packages/examples/src/examples/cynefin.ts @@ -26,7 +26,7 @@ export default { chaotic "Page on-call immediately" - confusion + aporetic "Unknown failure mode" complex --> complicated : "Pattern identified" diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index ead0597fe9e..c8ce1250d15 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -2018,6 +2018,13 @@ export interface CynefinDiagramConfig extends BaseDiagramConfig { * */ seed?: number; + /** + * Show flow arrows radiating outward from the central Aporetic domain to each + * of the four domains. This reflects that, in the Cynefin framework, flow moves + * outward from the Aporetic (not-yet-known) centre rather than in from one side. + * + */ + showFlow?: boolean; } /** * Configuration for Railroad (Syntax) Diagrams diff --git a/packages/mermaid/src/diagrams/cynefin/cynefin.integration.spec.ts b/packages/mermaid/src/diagrams/cynefin/cynefin.integration.spec.ts index aafdab0f436..e7e1b95ccc6 100644 --- a/packages/mermaid/src/diagrams/cynefin/cynefin.integration.spec.ts +++ b/packages/mermaid/src/diagrams/cynefin/cynefin.integration.spec.ts @@ -30,7 +30,7 @@ describe('Cynefin Parsing - Basic', () => { "C" chaotic "D" - confusion + aporetic "E" `); const domains = db.getDomains(); @@ -39,7 +39,7 @@ describe('Cynefin Parsing - Basic', () => { expect(domains.has('complicated')).toBe(true); expect(domains.has('clear')).toBe(true); expect(domains.has('chaotic')).toBe(true); - expect(domains.has('confusion')).toBe(true); + expect(domains.has('aporetic')).toBe(true); }); it('should parse empty domains', async () => { @@ -179,12 +179,12 @@ describe('Cynefin Parsing - Complex diagram', () => { "Deployment checklist" chaotic "Incident response" - confusion + aporetic "New initiative" complex --> complicated : "Pattern emerges" complicated --> clear : "Best practice found" chaotic --> complex : "Stabilized" - confusion --> chaotic : "Crisis detected" + aporetic --> chaotic : "Crisis detected" `); expect(db.getDiagramTitle()).toBe('Team Practices'); expect(db.getAccTitle()).toBe('Cynefin for team practices'); @@ -195,7 +195,7 @@ describe('Cynefin Parsing - Complex diagram', () => { expect(domains.get('complicated')!.items).toHaveLength(2); expect(domains.get('clear')!.items).toHaveLength(1); expect(domains.get('chaotic')!.items).toHaveLength(1); - expect(domains.get('confusion')!.items).toHaveLength(1); + expect(domains.get('aporetic')!.items).toHaveLength(1); const transitions = db.getTransitions(); expect(transitions).toHaveLength(4); @@ -205,7 +205,7 @@ describe('Cynefin Parsing - Complex diagram', () => { label: 'Pattern emerges', }); expect(transitions[3]).toMatchObject({ - from: 'confusion', + from: 'aporetic', to: 'chaotic', label: 'Crisis detected', }); diff --git a/packages/mermaid/src/diagrams/cynefin/cynefin.spec.ts b/packages/mermaid/src/diagrams/cynefin/cynefin.spec.ts index 020855357fb..8a9c27f8320 100644 --- a/packages/mermaid/src/diagrams/cynefin/cynefin.spec.ts +++ b/packages/mermaid/src/diagrams/cynefin/cynefin.spec.ts @@ -7,8 +7,9 @@ import { resolveSeed, generateFoldPath, generateHorizontalBoundary, + generateGradientBoundary, generateCliffPath, - generateConfusionPath, + generateAporeticPath, } from './cynefinBoundaries.js'; /** Test helper: build a partial DomainBlock with just the fields the DB reads. */ @@ -84,7 +85,7 @@ describe('Cynefin Database', () => { block('complicated'), block('clear'), block('chaotic'), - block('confusion'), + block('aporetic'), ]); expect(db.getDomains().size).toBe(5); }); @@ -100,6 +101,10 @@ describe('Cynefin Database', () => { const config = db.getConfig(); expect(typeof config).toBe('object'); }); + + it('should default showFlow to true', () => { + expect(db.getConfig().showFlow).toBe(true); + }); }); describe('Cynefin Boundaries', () => { @@ -147,15 +152,28 @@ describe('Cynefin Boundaries', () => { expect(path).toContain('C'); }); - it('generateConfusionPath should return valid ellipse path', () => { - const path = generateConfusionPath(400, 300, 50, 40); + it('generateGradientBoundary should return a straight line (no curves)', () => { + const path = generateGradientBoundary(400, 800, 300); + expect(path).toBe('M400,300 L800,300'); + expect(path).not.toContain('C'); + }); + + it('generateHorizontalBoundary should respect an explicit x-range and pin its endpoints', () => { + const path = generateHorizontalBoundary(800, 600, 42, 8, 0, 400); + // Pinned endpoints sit exactly on the centre line (cy = 300) at x=0 and x=400. + expect(path).toMatch(/^M0,300 /); + expect(path).toContain('400,300'); + }); + + it('generateAporeticPath should return valid ellipse path', () => { + const path = generateAporeticPath(400, 300, 50, 40); expect(path).toMatch(/^M/); expect(path).toContain('A'); expect(path).toMatch(/Z$/); }); - it('generateConfusionPath should use provided center and radii', () => { - const path = generateConfusionPath(400, 300, 50, 40); + it('generateAporeticPath should use provided center and radii', () => { + const path = generateAporeticPath(400, 300, 50, 40); expect(path).toContain('350'); // cx - rx = 400 - 50 expect(path).toContain('450'); // cx + rx = 400 + 50 expect(path).toContain('300'); // cy diff --git a/packages/mermaid/src/diagrams/cynefin/cynefinBoundaries.ts b/packages/mermaid/src/diagrams/cynefin/cynefinBoundaries.ts index c3c1343f431..7cc200d8d48 100644 --- a/packages/mermaid/src/diagrams/cynefin/cynefinBoundaries.ts +++ b/packages/mermaid/src/diagrams/cynefin/cynefinBoundaries.ts @@ -85,27 +85,40 @@ export function generateFoldPath( /** * Generate a horizontal wavy line through the center of the diagram. + * + * Defaults to spanning the full width, but accepts an x-range so a single side + * can be drawn. The Complex/Chaotic boundary (left half) is a genuine phase + * shift and uses this wavy line; the Clear/Complicated boundary (right half) is + * a gradient, not a phase shift, and is drawn separately via + * {@link generateGradientBoundary}. The range endpoints are pinned to the centre + * line so the wavy half meets the gradient half cleanly at the diagram centre. * @param width - diagram width * @param height - diagram height * @param seed - deterministic seed for variation * @param amplitudeOverride - optional amplitude in pixels; falls back to 1.5% of height + * @param xStart - left edge of the segment (defaults to 0) + * @param xEnd - right edge of the segment (defaults to width) * @returns SVG path d attribute string */ export function generateHorizontalBoundary( width: number, height: number, seed: number, - amplitudeOverride?: number + amplitudeOverride?: number, + xStart = 0, + xEnd = width ): string { const cy = height / 2; const amplitude = amplitudeOverride ?? height * 0.015; const segments = 7; - const segWidth = width / segments; + const segWidth = (xEnd - xStart) / segments; const points: { x: number; y: number }[] = []; for (let i = 0; i <= segments; i++) { - const jitter = seededRandom(seed + i * 23) * amplitude * 2 - amplitude; - points.push({ x: i * segWidth, y: cy + jitter }); + // Pin the endpoints to the centre line so adjoining segments meet cleanly. + const jitter = + i === 0 || i === segments ? 0 : seededRandom(seed + i * 23) * amplitude * 2 - amplitude; + points.push({ x: xStart + i * segWidth, y: cy + jitter }); } let d = `M${points[0].x},${points[0].y}`; @@ -125,6 +138,22 @@ export function generateHorizontalBoundary( return d; } +/** + * Generate a plain straight boundary line between two ordered domains. + * + * Used for the Clear/Complicated boundary, which is a gradient rather than a + * phase shift — unlike the central fold, the Complex/Chaotic boundary, and the + * Clear/Chaotic cliff, crossing it is a smooth, reversible transition, so it is + * drawn as a simple straight line with no waviness. + * @param xStart - left edge of the line + * @param xEnd - right edge of the line + * @param y - vertical position of the line + * @returns SVG path d attribute string + */ +export function generateGradientBoundary(xStart: number, xEnd: number, y: number): string { + return `M${xStart},${y} L${xEnd},${y}`; +} + /** * Generate the "cliff" path between Clear (bottom-right) and Chaotic (bottom-left). * This is a thicker, more abrupt boundary near the bottom center. @@ -151,14 +180,14 @@ export function generateCliffPath(width: number, height: number): string { } /** - * Generate an ellipse SVG path for the confusion/disorder region at the center. + * Generate an ellipse SVG path for the Aporetic region at the center. * @param cx - center x * @param cy - center y * @param rx - horizontal radius * @param ry - vertical radius * @returns SVG path d attribute string for an ellipse */ -export function generateConfusionPath(cx: number, cy: number, rx: number, ry: number): string { +export function generateAporeticPath(cx: number, cy: number, rx: number, ry: number): string { // Draw an ellipse using two arc commands return [ `M${cx - rx},${cy}`, diff --git a/packages/mermaid/src/diagrams/cynefin/cynefinRenderer.ts b/packages/mermaid/src/diagrams/cynefin/cynefinRenderer.ts index c8951527192..9688efdadd5 100644 --- a/packages/mermaid/src/diagrams/cynefin/cynefinRenderer.ts +++ b/packages/mermaid/src/diagrams/cynefin/cynefinRenderer.ts @@ -10,8 +10,9 @@ import type { CynefinDB, CynefinDomain, CynefinTransition, DomainName } from './ import { generateFoldPath, generateHorizontalBoundary, + generateGradientBoundary, generateCliffPath, - generateConfusionPath, + generateAporeticPath, resolveSeed, } from './cynefinBoundaries.js'; @@ -25,7 +26,7 @@ const DOMAIN_META: Record = { complicated: { model: 'Sense \u2192 Analyse \u2192 Respond', practice: 'Good Practices' }, clear: { model: 'Sense \u2192 Categorise \u2192 Respond', practice: 'Best Practices' }, chaotic: { model: 'Act \u2192 Sense \u2192 Respond', practice: 'Novel Practices' }, - confusion: { model: '', practice: 'Disorder' }, + aporetic: { model: '', practice: '' }, }; interface DomainLayout { @@ -45,7 +46,7 @@ const getDomainLayouts = (width: number, height: number): Record { return themeVariables.cynefin as CynefinDomainColors; }; -/** Maximum items rendered inside the confusion ellipse before overflow badge is shown. */ -const MAX_CONFUSION_ITEMS = 3; +/** Maximum items rendered inside the aporetic ellipse before overflow badge is shown. */ +const MAX_APORETIC_ITEMS = 3; const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { const db = diagram.db as CynefinDB; @@ -85,6 +86,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { const padding = config.padding; const showDomainDescriptions = config.showDomainDescriptions; const boundaryAmplitude = config.boundaryAmplitude; + const showFlow = config.showFlow; const totalWidth = width + padding * 2; const totalHeight = height + padding * 2; @@ -93,7 +95,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { complicated: domainColors.complicatedBg, clear: domainColors.clearBg, chaotic: domainColors.chaoticBg, - confusion: domainColors.confusionBg, + aporetic: domainColors.aporeticBg, }; const svg: SVG = selectSvgElement(id); @@ -140,10 +142,22 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { .attr('d', generateFoldPath(width, height, seed, boundaryAmplitude)) .attr('fill', 'none'); + // Horizontal boundary splits into two halves with different meanings: + // - left half (Complex/Chaotic) is a genuine phase shift → wavy + // - right half (Clear/Complicated) is a gradient, not a phase shift → straight boundaryGroup .append('path') .attr('class', 'cynefinBoundary') - .attr('d', generateHorizontalBoundary(width, height, seed + 100, boundaryAmplitude)) + .attr( + 'd', + generateHorizontalBoundary(width, height, seed + 100, boundaryAmplitude, 0, width / 2) + ) + .attr('fill', 'none'); + + boundaryGroup + .append('path') + .attr('class', 'cynefinGradientBoundary') + .attr('d', generateGradientBoundary(width / 2, width, height / 2)) .attr('fill', 'none'); // 3. The cliff (thicker, between Clear and Chaotic) — stroke handled by .cynefinCliff CSS class @@ -153,17 +167,57 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { .attr('d', generateCliffPath(width, height)) .attr('fill', 'none'); - // 4. Confusion ellipse (center overlay) — stroke handled by .cynefinConfusion CSS class + // 4. Aporetic ellipse (center overlay) — stroke handled by .cynefinAporetic CSS class // Using width*0.15 and height*0.15 gives enough room for up to ~3 item badges - const confusionRx = width * 0.15; - const confusionRy = height * 0.15; + const aporeticRx = width * 0.15; + const aporeticRy = height * 0.15; root .append('path') - .attr('class', 'cynefinConfusion') - .attr('d', generateConfusionPath(width / 2, height / 2, confusionRx, confusionRy)) - .attr('fill', domainBg.confusion) + .attr('class', 'cynefinAporetic') + .attr('d', generateAporeticPath(width / 2, height / 2, aporeticRx, aporeticRy)) + .attr('fill', domainBg.aporetic) .attr('fill-opacity', 0.5); + // 4b. Flow arrows radiating outward from the Aporetic centre to each domain. + // In Cynefin, flow moves outward from the not-yet-known centre, not in from a side. + if (showFlow) { + const flowDefs = svg.select('defs').empty() ? svg.append('defs') : svg.select('defs'); + const flowMarkerId = `cynefin-flow-${id}`; + flowDefs + .append('marker') + .attr('id', flowMarkerId) + .attr('viewBox', '0 0 10 10') + .attr('refX', 9) + .attr('refY', 5) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto-start-reverse') + .append('path') + .attr('d', 'M 0 0 L 10 5 L 0 10 z') + .attr('class', 'cynefinFlowHead'); + + const flowGroup = root.append('g').attr('class', 'cynefin-flow'); + const cx = width / 2; + const cy = height / 2; + for (const domainName of quadrantDomains) { + const target = layouts[domainName]; + const dx = target.cx - cx; + const dy = target.cy - cy; + const len = Math.sqrt(dx * dx + dy * dy); + // Start at the Aporetic ellipse edge, end partway toward the quadrant centre. + const startX = cx + (dx / len) * aporeticRx; + const startY = cy + (dy / len) * aporeticRy; + const endX = cx + dx * 0.7; + const endY = cy + dy * 0.7; + flowGroup + .append('path') + .attr('class', 'cynefinFlowLine') + .attr('d', `M${startX},${startY} L${endX},${endY}`) + .attr('fill', 'none') + .attr('marker-end', `url(#${flowMarkerId})`); + } + } + // 5. Domain name labels — text styling handled by .cynefinDomainLabel CSS class const labelGroup = root.append('g').attr('class', 'cynefin-labels'); for (const domainName of quadrantDomains) { @@ -178,7 +232,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { .text(domainName.charAt(0).toUpperCase() + domainName.slice(1)); } - // Confusion label + // Aporetic label labelGroup .append('text') .attr('class', 'cynefinDomainLabel') @@ -186,7 +240,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { .attr('y', showDomainDescriptions ? height / 2 - 10 : height / 2) .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') - .text('Confusion'); + .text('Aporetic'); // 6. Domain description subtitles — text styling handled by .cynefinSubtitle CSS class if (showDomainDescriptions) { @@ -213,16 +267,8 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { .attr('dominant-baseline', 'middle') .text(meta.practice); } - - // Confusion subtitle - subtitleGroup - .append('text') - .attr('class', 'cynefinSubtitle') - .attr('x', width / 2) - .attr('y', height / 2 + 8) - .attr('text-anchor', 'middle') - .attr('dominant-baseline', 'middle') - .text(DOMAIN_META.confusion.practice); + // The Aporetic centre has no decision model or practice subtitle — it is the + // state of not-yet-knowing which domain applies, so only its label is shown. } // 7. Items as text badges within each domain @@ -230,7 +276,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { const itemHeight = 26; const itemPaddingX = 10; - const allDomains: DomainName[] = ['complex', 'complicated', 'chaotic', 'clear', 'confusion']; + const allDomains: DomainName[] = ['complex', 'complicated', 'chaotic', 'clear', 'aporetic']; for (const domainName of allDomains) { const domain: CynefinDomain | undefined = domains.get(domainName); if (!domain || domain.items.length === 0) { @@ -238,19 +284,19 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { } const layout = layouts[domainName]; - const isConfusion = domainName === 'confusion'; + const isAporetic = domainName === 'aporetic'; - // For confusion: cap items and center the block around the ellipse center. + // For aporetic: cap items and center the block around the ellipse center. // For quadrant domains: start below the label/subtitle area. let itemsToRender = domain.items; let overflowCount = 0; - if (isConfusion && domain.items.length > MAX_CONFUSION_ITEMS) { - overflowCount = domain.items.length - MAX_CONFUSION_ITEMS; - itemsToRender = domain.items.slice(0, MAX_CONFUSION_ITEMS); + if (isAporetic && domain.items.length > MAX_APORETIC_ITEMS) { + overflowCount = domain.items.length - MAX_APORETIC_ITEMS; + itemsToRender = domain.items.slice(0, MAX_APORETIC_ITEMS); } let startY: number; - if (isConfusion) { + if (isAporetic) { // Center the item block below the label within the ellipse const labelOffset = showDomainDescriptions ? 22 : 14; startY = layout.cy + labelOffset; @@ -304,7 +350,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { textEl.attr('x', badgeWidth / 2).attr('y', itemHeight / 2); }); - // Overflow badge: "+N more" when confusion has more items than MAX_CONFUSION_ITEMS + // Overflow badge: "+N more" when aporetic has more items than MAX_APORETIC_ITEMS if (overflowCount > 0) { const overflowY = startY + itemsToRender.length * (itemHeight + 4); const overflowLabel = `+${overflowCount} more`; diff --git a/packages/mermaid/src/diagrams/cynefin/styles.ts b/packages/mermaid/src/diagrams/cynefin/styles.ts index d0b7b48140f..4286add0554 100644 --- a/packages/mermaid/src/diagrams/cynefin/styles.ts +++ b/packages/mermaid/src/diagrams/cynefin/styles.ts @@ -62,11 +62,16 @@ export const styles: DiagramStylesProvider = () => { stroke-width: ${t.boundaryWidth}; stroke-dasharray: 6 3; } + .cynefinGradientBoundary { + stroke: ${t.boundaryColor}; + stroke-width: ${t.boundaryWidth}; + stroke-opacity: 0.4; + } .cynefinCliff { stroke: ${t.cliffColor}; stroke-width: ${t.cliffWidth}; } - .cynefinConfusion { + .cynefinAporetic { stroke: ${t.boundaryColor}; stroke-width: 1.5; stroke-dasharray: 4 2; @@ -76,6 +81,18 @@ export const styles: DiagramStylesProvider = () => { stroke-width: ${t.arrowWidth}; fill: none; } + .cynefinFlowLine { + stroke: ${t.arrowColor}; + stroke-width: ${t.arrowWidth}; + stroke-dasharray: 5 3; + stroke-opacity: 0.5; + fill: none; + } + .cynefinFlowHead { + fill: ${t.arrowColor}; + fill-opacity: 0.5; + stroke: none; + } .cynefinArrowHead { fill: ${t.arrowColor}; stroke: none; diff --git a/packages/mermaid/src/diagrams/cynefin/types.ts b/packages/mermaid/src/diagrams/cynefin/types.ts index abeaf493209..24bd533a269 100644 --- a/packages/mermaid/src/diagrams/cynefin/types.ts +++ b/packages/mermaid/src/diagrams/cynefin/types.ts @@ -1,7 +1,7 @@ import type { DomainBlock, Transition } from '@mermaid-js/parser'; import type { CynefinDiagramConfig } from '../../config.type.js'; -export type DomainName = 'complex' | 'complicated' | 'clear' | 'chaotic' | 'confusion'; +export type DomainName = 'complex' | 'complicated' | 'clear' | 'chaotic' | 'aporetic'; export interface CynefinItem { label: string; diff --git a/packages/mermaid/src/docs/syntax/cynefin.md b/packages/mermaid/src/docs/syntax/cynefin.md index 2fbb6e4e667..5cc970b3776 100644 --- a/packages/mermaid/src/docs/syntax/cynefin.md +++ b/packages/mermaid/src/docs/syntax/cynefin.md @@ -12,9 +12,9 @@ The Cynefin framework divides the world into five domains, each with its own dec - **Complicated**: Cause and effect require analysis or expertise. Sense → Analyse → Respond. Apply **good practices**. - **Complex**: Cause and effect can only be deduced in retrospect. Probe → Sense → Respond. Apply **emergent practices**. - **Chaotic**: No perceivable cause and effect. Act → Sense → Respond. Apply **novel practices**. -- **Confusion** (or Disorder): You do not know which domain you are in. The goal is to move items out of this state into one of the other four. +- **Aporetic** (the central domain): You do not yet know which domain you are in — a state of productive not-knowing. The goal is to move items out of this state into one of the other four. (Note: "Confused" is a distinct concept that only appears in the liminal/dynamic version of Cynefin, which this diagram does not model.) -The signature visual feature is the wavy, organic boundary between the ordered (Clear, Complicated) and unordered (Complex, Chaotic) halves, and the "cliff" between Clear and Chaotic representing the risk of complacency leading to crisis. +The signature visual features are the phase-shift boundaries between domains — wavy and abrupt where crossing them is a real transition (the central fold, the Complex/Chaotic boundary, and the "cliff" between Clear and Chaotic) — versus the plain Clear/Complicated boundary, which is a gradient rather than a phase shift. Flow radiates outward from the central Aporetic domain. ## Syntax @@ -37,7 +37,7 @@ clear chaotic "Crisis response" -confusion +aporetic "Item of unknown domain" complex --> complicated : "Pattern identified" @@ -54,7 +54,7 @@ clear --> chaotic : "Complacency" | `complicated` | Opens the Complicated domain block | | `clear` | Opens the Clear domain block | | `chaotic` | Opens the Chaotic domain block | -| `confusion` | Opens the Confusion / Disorder domain block | +| `aporetic` | Opens the central Aporetic (not-yet-known) domain block | | `-->` | Declares a transition from one domain to another | ### Items @@ -67,7 +67,7 @@ complex "Run chaos experiment" ``` -Keep per-domain item lists short — the quadrants have fixed layout and long lists can visually overflow their boxes. The confusion ellipse caps at three items and shows a `+N more` badge; the four quadrant domains do not clip, so prefer a handful of items each. +Keep per-domain item lists short — the quadrants have fixed layout and long lists can visually overflow their boxes. The aporetic ellipse caps at three items and shows a `+N more` badge; the four quadrant domains do not clip, so prefer a handful of items each. ### Transitions @@ -109,7 +109,7 @@ cynefin-beta chaotic "Page on-call immediately" - confusion + aporetic "Unknown failure mode" ``` @@ -163,6 +163,7 @@ Cynefin diagrams accept the following configuration under the `cynefin` key in t | `showDomainDescriptions` | boolean | `true` | Show decision model and practice type subtitles per domain | | `boundaryAmplitude` | number | `8` | Waviness amplitude of domain boundaries in pixels (set to `0` for straight lines) | | `seed` | number | `0` | Deterministic seed for boundary waviness. `0` (default) hashes the diagram's SVG id so each diagram looks unique. Set any non-zero number to lock the waviness across renders — required for stable visual regression tests. | +| `showFlow` | boolean | `true` | Show flow arrows radiating outward from the central Aporetic domain to each domain (set to `false` to hide them) | Example: @@ -177,29 +178,29 @@ cynefin-beta Cynefin diagrams use the following theme variables, which can be overridden via `themeVariables.cynefin`: -| Variable | Description | -| ---------------- | ------------------------------------------------ | -| `complexBg` | Background color for the Complex domain | -| `complicatedBg` | Background color for the Complicated domain | -| `clearBg` | Background color for the Clear domain | -| `chaoticBg` | Background color for the Chaotic domain | -| `confusionBg` | Background color for the Confusion center region | -| `boundaryColor` | Color of the wavy domain boundaries | -| `boundaryWidth` | Stroke width of the boundaries | -| `cliffColor` | Color of the Clear/Chaotic cliff | -| `cliffWidth` | Stroke width of the cliff | -| `arrowColor` | Color of transition arrows | -| `arrowWidth` | Stroke width of transition arrows | -| `labelColor` | Color of domain name labels | -| `textColor` | Color of item and subtitle text | -| `domainFontSize` | Font size of domain name labels | -| `itemFontSize` | Font size of item badges and subtitles | +| Variable | Description | +| ---------------- | ----------------------------------------------- | +| `complexBg` | Background color for the Complex domain | +| `complicatedBg` | Background color for the Complicated domain | +| `clearBg` | Background color for the Clear domain | +| `chaoticBg` | Background color for the Chaotic domain | +| `aporeticBg` | Background color for the Aporetic center region | +| `boundaryColor` | Color of the wavy domain boundaries | +| `boundaryWidth` | Stroke width of the boundaries | +| `cliffColor` | Color of the Clear/Chaotic cliff | +| `cliffWidth` | Stroke width of the cliff | +| `arrowColor` | Color of transition arrows | +| `arrowWidth` | Stroke width of transition arrows | +| `labelColor` | Color of domain name labels | +| `textColor` | Color of item and subtitle text | +| `domainFontSize` | Font size of domain name labels | +| `itemFontSize` | Font size of item badges and subtitles | ## Notes -- Domain names are fixed keywords. Only `complex`, `complicated`, `clear`, `chaotic`, and `confusion` are recognized. -- Domains can be declared in any order; their position in the diagram is always the same (Complex top-left, Complicated top-right, Chaotic bottom-left, Clear bottom-right, Confusion center). -- The `confusion` domain has a compact center ellipse. Up to 3 items are shown inside it; if more are provided a `+N more` overflow badge is displayed. In practice, the confusion domain should contain very few items — its purpose is to surface unknowns so they can be moved to one of the four main domains. +- Domain names are fixed keywords. Only `complex`, `complicated`, `clear`, `chaotic`, and `aporetic` are recognized. +- Domains can be declared in any order; their position in the diagram is always the same (Complex top-left, Complicated top-right, Chaotic bottom-left, Clear bottom-right, Aporetic center). +- The `aporetic` domain has a compact center ellipse. Up to 3 items are shown inside it; if more are provided a `+N more` overflow badge is displayed. In practice, the aporetic domain should contain very few items — its purpose is to surface unknowns so they can be moved to one of the four main domains. - Self-loop transitions (e.g. `complex --> complex`) are silently ignored. Transitions must connect two different domains. - Handdrawn mode is not currently supported. - The wavy boundary rendering is deterministic: the same input always produces the same diagram, so diffs are stable across builds. diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 921962a1dc7..066018a81f6 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -2781,6 +2781,13 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) regression tests to be stable. type: number default: 0 + showFlow: + description: | + Show flow arrows radiating outward from the central Aporetic domain to each + of the four domains. This reflects that, in the Cynefin framework, flow moves + outward from the Aporetic (not-yet-known) centre rather than in from one side. + type: boolean + default: true RailroadDiagramConfig: title: Railroad Diagram Config diff --git a/packages/mermaid/src/themes/theme-base.js b/packages/mermaid/src/themes/theme-base.js index 042f4cda981..be54328824b 100644 --- a/packages/mermaid/src/themes/theme-base.js +++ b/packages/mermaid/src/themes/theme-base.js @@ -266,7 +266,7 @@ class Theme { complicatedBg: this.cynefin?.complicatedBg || '#E3F2FD', chaoticBg: this.cynefin?.chaoticBg || '#FBE9E7', clearBg: this.cynefin?.clearBg || '#FFF8E1', - confusionBg: this.cynefin?.confusionBg || '#F3E5F5', + aporeticBg: this.cynefin?.aporeticBg || '#F3E5F5', textColor: this.cynefin?.textColor || this.textColor, labelColor: this.cynefin?.labelColor || this.primaryTextColor, }; diff --git a/packages/mermaid/src/themes/theme-dark.js b/packages/mermaid/src/themes/theme-dark.js index 276c79b0456..936e8872c6a 100644 --- a/packages/mermaid/src/themes/theme-dark.js +++ b/packages/mermaid/src/themes/theme-dark.js @@ -273,7 +273,7 @@ class Theme { complicatedBg: this.cynefin?.complicatedBg || '#0D47A1', chaoticBg: this.cynefin?.chaoticBg || '#BF360C', clearBg: this.cynefin?.clearBg || '#F57F17', - confusionBg: this.cynefin?.confusionBg || '#4A148C', + aporeticBg: this.cynefin?.aporeticBg || '#4A148C', textColor: this.cynefin?.textColor || this.textColor, labelColor: this.cynefin?.labelColor || this.primaryTextColor, }; diff --git a/packages/mermaid/src/themes/theme-default.js b/packages/mermaid/src/themes/theme-default.js index 9ba0a2afcac..5fd142701d3 100644 --- a/packages/mermaid/src/themes/theme-default.js +++ b/packages/mermaid/src/themes/theme-default.js @@ -306,7 +306,7 @@ class Theme { complicatedBg: this.cynefin?.complicatedBg || '#E3F2FD', chaoticBg: this.cynefin?.chaoticBg || '#FBE9E7', clearBg: this.cynefin?.clearBg || '#FFF8E1', - confusionBg: this.cynefin?.confusionBg || '#F3E5F5', + aporeticBg: this.cynefin?.aporeticBg || '#F3E5F5', textColor: this.cynefin?.textColor || this.textColor, labelColor: this.cynefin?.labelColor || this.primaryTextColor, }; diff --git a/packages/mermaid/src/themes/theme-forest.js b/packages/mermaid/src/themes/theme-forest.js index cfb36d2e9a9..49bf8417221 100644 --- a/packages/mermaid/src/themes/theme-forest.js +++ b/packages/mermaid/src/themes/theme-forest.js @@ -269,7 +269,7 @@ class Theme { complicatedBg: this.cynefin?.complicatedBg || '#DCEDC8', chaoticBg: this.cynefin?.chaoticBg || '#FFE0B2', clearBg: this.cynefin?.clearBg || '#FFF9C4', - confusionBg: this.cynefin?.confusionBg || '#D7CCC8', + aporeticBg: this.cynefin?.aporeticBg || '#D7CCC8', textColor: this.cynefin?.textColor || this.textColor, labelColor: this.cynefin?.labelColor || this.primaryTextColor, }; diff --git a/packages/mermaid/src/themes/theme-neutral.js b/packages/mermaid/src/themes/theme-neutral.js index 8e8741df7e0..54c440ebb8d 100644 --- a/packages/mermaid/src/themes/theme-neutral.js +++ b/packages/mermaid/src/themes/theme-neutral.js @@ -294,7 +294,7 @@ class Theme { complicatedBg: this.cynefin?.complicatedBg || '#E3F2FD', chaoticBg: this.cynefin?.chaoticBg || '#FBE9E7', clearBg: this.cynefin?.clearBg || '#FFF8E1', - confusionBg: this.cynefin?.confusionBg || '#F3E5F5', + aporeticBg: this.cynefin?.aporeticBg || '#F3E5F5', textColor: this.cynefin?.textColor || this.textColor, labelColor: this.cynefin?.labelColor || this.primaryTextColor, }; diff --git a/packages/parser/src/language/cynefin/cynefin.langium b/packages/parser/src/language/cynefin/cynefin.langium index b95d0115b1a..d6e705c817f 100644 --- a/packages/parser/src/language/cynefin/cynefin.langium +++ b/packages/parser/src/language/cynefin/cynefin.langium @@ -27,4 +27,4 @@ Transition: from=DOMAIN_NAME '-->' to=DOMAIN_NAME (':' label=STRING)? EOL ; -terminal DOMAIN_NAME returns string: 'complex' | 'complicated' | 'clear' | 'chaotic' | 'confusion'; +terminal DOMAIN_NAME returns string: 'complex' | 'complicated' | 'clear' | 'chaotic' | 'aporetic';