diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
index d62c11a..00c8335 100644
--- a/backend/src/routes/auth.js
+++ b/backend/src/routes/auth.js
@@ -222,14 +222,25 @@ router.post('/logout',
/**
* GET /api/auth/session
- * Get current session info
+ * Probe the current session state. Always responds with HTTP 200 — the
+ * absence of a session is a legitimate state, not an error. Clients
+ * (frontend AuthContext) read `authenticated` to decide whether to
+ * surface a logged-in UI or redirect to the login page.
+ *
+ * Returning 401 here would force the frontend to console.error on every
+ * unauthenticated page load, polluting logs and dev tools without
+ * carrying real information.
*/
router.get('/session',
- validateSession,
asyncHandler(async (req, res) => {
- const user = req.user;
-
- res.json({
+ if (!req.session || !req.session.userId) {
+ return res.json({ authenticated: false, user: null });
+ }
+ const user = await User.findByPk(req.session.userId);
+ if (!user || !user.is_active) {
+ return res.json({ authenticated: false, user: null });
+ }
+ return res.json({
authenticated: true,
user: {
id: user.id,
diff --git a/backend/src/routes/public.js b/backend/src/routes/public.js
index 3b3ff2e..0ed61aa 100644
--- a/backend/src/routes/public.js
+++ b/backend/src/routes/public.js
@@ -183,21 +183,31 @@ router.post('/parse-dot',
label: node.attributes.label || node.id,
name: node.attributes.label || node.id,
size: node.attributes.val || node.attributes.size || '8',
- color: node.attributes.color || '#1976D2',
+ // Pass color through only when the user actually specified it in DOT.
+ // The frontend applies a default (and role-based tint for DES graphs),
+ // so we must not paper over the user's intent here.
+ color: node.attributes.color,
geometry: node.attributes.geometry,
dimensions: node.attributes.dimensions,
particleGeneration: node.attributes.particleGeneration,
maxParticleProcessing: node.attributes.maxParticleProcessing,
image: node.attributes.image,
autoResize: node.attributes.autoResize,
- bloomEffect: node.attributes.bloomEffect
+ bloomEffect: node.attributes.bloomEffect,
+ // DES attributes (ADR-006) — consumed by the in-browser ParticleSimulator
+ nodeRole: node.attributes.nodeRole,
+ dropPolicy: node.attributes.dropPolicy,
+ queue_size: node.attributes.queue_size,
+ processing_time: node.attributes.processing_time,
+ failure_rate: node.attributes.failure_rate
}));
-
+
const links = parseResult.ast.edges.map(edge => ({
source: edge.from,
target: edge.to,
label: edge.attributes.label || '',
- color: edge.attributes.color || '#888',
+ // Same reasoning as for node.color: pass through only when set.
+ color: edge.attributes.color,
maxParticleFlow: edge.attributes.maxParticleFlow,
particleSpeed: edge.attributes.particleSpeed,
style: edge.attributes.style || 'solid'
diff --git a/backend/src/utils/dotValidator.js b/backend/src/utils/dotValidator.js
index e1322ab..99ac57d 100644
--- a/backend/src/utils/dotValidator.js
+++ b/backend/src/utils/dotValidator.js
@@ -19,9 +19,15 @@ class DotValidator {
// VortexFlow 3D extensions
'geometry', 'dimensions', 'particleGeneration', 'maxParticleProcessing',
'particleSpeed', 'maxParticleFlow', 'image', 'autoResize',
- 'bloomEffect', 'particlesEnabled', 'autoColors', 'defaultNodeSize'
+ 'bloomEffect', 'particlesEnabled', 'autoColors', 'defaultNodeSize',
+ // DES (Discrete Event Simulation) attributes — ADR-006
+ 'nodeRole', 'dropPolicy'
]);
+ // DES enums (ADR-006)
+ this.nodeRoles = new Set(['generator', 'relay', 'sink']);
+ this.dropPolicies = new Set(['tail', 'head', 'reject']);
+
this.nodeShapes = new Set([
// Standard DOT shapes
'box', 'circle', 'ellipse', 'point', 'egg', 'triangle',
@@ -118,6 +124,10 @@ class DotValidator {
result.metadata.hasVortexFlowExtensions = extensionResult.hasExtensions;
result.warnings.push(...extensionResult.warnings);
+ // DES coherence checks (ADR-006) — cross-attribute warnings on the parsed AST
+ const coherenceResult = this.validateDESCoherence(parseResult.ast);
+ result.warnings.push(...coherenceResult.warnings);
+
// Performance warnings
this.addPerformanceWarnings(result);
@@ -571,7 +581,9 @@ class DotValidator {
const vortexFlow3DAttrs = [
'geometry', 'dimensions', 'particleGeneration', 'maxParticleProcessing',
'particleSpeed', 'maxParticleFlow', 'image', 'autoResize',
- 'bloomEffect', 'particlesEnabled', 'autoColors', 'defaultNodeSize'
+ 'bloomEffect', 'particlesEnabled', 'autoColors', 'defaultNodeSize',
+ // DES — ADR-006
+ 'nodeRole', 'dropPolicy'
];
// Check legacy attributes
@@ -710,6 +722,60 @@ class DotValidator {
result.warnings.push(`Image path "${value}" may not be a valid image file`);
}
break;
+
+ case 'nodeRole':
+ if (!this.nodeRoles.has(value)) {
+ result.warnings.push(
+ `Invalid nodeRole: "${value}". Valid roles: ${Array.from(this.nodeRoles).join(', ')}`
+ );
+ }
+ break;
+
+ case 'dropPolicy':
+ if (!this.dropPolicies.has(value)) {
+ result.warnings.push(
+ `Invalid dropPolicy: "${value}". Valid policies: ${Array.from(this.dropPolicies).join(', ')}`
+ );
+ }
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Validate cross-attribute coherence for DES (ADR-006).
+ *
+ * These are warnings, not errors — the graph is still accepted, but
+ * the validator surfaces the inconsistency so the user understands why
+ * the runtime ignores a given attribute.
+ */
+ validateDESCoherence(ast) {
+ const result = { warnings: [] };
+
+ for (const node of ast.nodes) {
+ const attrs = node.attributes || {};
+ const role = attrs.nodeRole;
+ const dropPolicy = attrs.dropPolicy;
+ const queueSize = attrs.queue_size;
+ const particleGeneration = attrs.particleGeneration;
+
+ // dropPolicy without queue_size: meaningless (queue is unbounded)
+ if (dropPolicy !== undefined && queueSize === undefined) {
+ result.warnings.push(
+ `Node "${node.id}": dropPolicy="${dropPolicy}" has no effect without queue_size — the queue is unbounded and never drops.`
+ );
+ }
+
+ // particleGeneration > 0 on a non-generator role: ignored at runtime
+ if (particleGeneration !== undefined && role !== undefined && role !== 'generator') {
+ const numGen = parseFloat(particleGeneration);
+ if (!Number.isNaN(numGen) && numGen > 0) {
+ result.warnings.push(
+ `Node "${node.id}": particleGeneration=${particleGeneration} is ignored because nodeRole="${role}" (only "generator" emits).`
+ );
+ }
}
}
diff --git a/backend/src/utils/setup.js b/backend/src/utils/setup.js
index 7de81a3..21ffe10 100644
--- a/backend/src/utils/setup.js
+++ b/backend/src/utils/setup.js
@@ -113,108 +113,174 @@ const createSampleData = async () => {
return;
}
- // Create sample graphs
+ // VortexFlow Showcase — single demo graph exercising every documented
+ // capability: the three nodeRoles (generator / relay / sink), all five 3D
+ // geometries (Sphere / Box / Cylinder / Cone / Torus), both dropPolicies
+ // (tail / head), failure_rate, weighted maxParticleFlow routing, variable
+ // particleSpeed, queue saturation and legacy attributes (bandwidth /
+ // capacity / latency). Used as the out-of-the-box demo when an admin
+ // first opens the app.
const sampleGraphs = [
{
- title: 'Simple Network Flow',
- description: 'A basic network topology with routers and servers',
- dot_code: `digraph NetworkFlow {
+ title: 'VortexFlow Showcase',
+ description: 'Démonstration complète : générateurs, relais, sinks, '
+ + 'les 5 géométries 3D, drop policies, failure_rate, routage pondéré '
+ + 'et saturation de file. Le pipeline simule un flux IoT : capteurs '
+ + '→ load balancer → cluster de processeurs (un instable, un lent) '
+ + '→ cache mémoire → archive rapide / analytics avec filtre saturé.',
+ dot_code: `digraph VortexFlowShowcase {
rankdir=LR;
-
- // Nodes with simulation properties
- Router1 [label="Router A", capacity=100, processing_time=1.0, node_type="router"]
- Server1 [label="Web Server", capacity=50, processing_time=2.0, node_type="server"]
- Server2 [label="Database", capacity=200, processing_time=0.5, node_type="database"]
-
- // Edges with flow properties
- Router1 -> Server1 [bandwidth=10, latency=0.2, data_type="http_requests"]
- Server1 -> Server2 [bandwidth=5, latency=0.1, data_type="db_queries"]
+ defaultNodeSize=1.2;
+ particlesEnabled=true;
+ autoColors=false;
+
+ // ---------------------------------------------------------------
+ // Generators — IoT sensors emitting telemetry packets (Cone)
+ // ---------------------------------------------------------------
+ SensorA [
+ label="Capteur A",
+ nodeRole="generator",
+ particleGeneration=3,
+ geometry="Cone",
+ dimensions="{radius: 0.8, height: 1.6}",
+ color="#ff6b35"
+ ];
+
+ SensorB [
+ label="Capteur B",
+ nodeRole="generator",
+ particleGeneration=2,
+ geometry="Cone",
+ dimensions="{radius: 0.8, height: 1.6}",
+ color="#ffa726"
+ ];
+
+ // ---------------------------------------------------------------
+ // Load balancer — weighted dispatch (Torus ring)
+ // ---------------------------------------------------------------
+ LoadBalancer [
+ label="Load Balancer",
+ nodeRole="relay",
+ maxParticleProcessing=20,
+ queue_size=50,
+ processing_time=100,
+ geometry="Torus",
+ dimensions="{radius: 1.2, tube: 0.4}",
+ color="#42a5f5"
+ ];
+
+ // ---------------------------------------------------------------
+ // Compute cluster — three parallel processors (Cylinder).
+ // P1 = nominal, P2 = unstable (failure_rate), P3 = slow goulot.
+ // ---------------------------------------------------------------
+ Processor1 [
+ label="Processeur P1",
+ nodeRole="relay",
+ maxParticleProcessing=5,
+ queue_size=20,
+ processing_time=300,
+ geometry="Cylinder",
+ dimensions="{radius: 0.7, height: 1.8}",
+ color="#66bb6a"
+ ];
+
+ Processor2 [
+ label="Processeur P2 (instable)",
+ nodeRole="relay",
+ maxParticleProcessing=4,
+ queue_size=15,
+ processing_time=400,
+ failure_rate=0.25,
+ geometry="Cylinder",
+ dimensions="{radius: 0.7, height: 1.8}",
+ color="#ef5350"
+ ];
+
+ Processor3 [
+ label="Processeur P3 (lent)",
+ nodeRole="relay",
+ maxParticleProcessing=1,
+ queue_size=10,
+ processing_time=800,
+ geometry="Cylinder",
+ dimensions="{radius: 0.7, height: 1.8}",
+ color="#ab47bc"
+ ];
+
+ // ---------------------------------------------------------------
+ // In-memory cache — fast aggregator (Sphere)
+ // ---------------------------------------------------------------
+ Cache [
+ label="Cache mémoire",
+ nodeRole="relay",
+ maxParticleProcessing=30,
+ queue_size=100,
+ processing_time=50,
+ geometry="Sphere",
+ dimensions="{radius: 1.0}",
+ color="#26c6da"
+ ];
+
+ // ---------------------------------------------------------------
+ // Analytics filter — saturated relay, dropPolicy="tail"
+ // ---------------------------------------------------------------
+ Filter [
+ label="Filtre Analytics",
+ nodeRole="relay",
+ maxParticleProcessing=2,
+ queue_size=5,
+ dropPolicy="tail",
+ processing_time=1500,
+ geometry="Cylinder",
+ dimensions="{radius: 0.6, height: 1.4}",
+ color="#ffca28"
+ ];
+
+ // ---------------------------------------------------------------
+ // Sinks — terminal storage (Box). Analytics also demos dropPolicy="head".
+ // ---------------------------------------------------------------
+ Archive [
+ label="Archive S3",
+ nodeRole="sink",
+ geometry="Box",
+ dimensions="{width: 2.0, height: 1.4, depth: 1.4}",
+ color="#5c6bc0"
+ ];
+
+ Analytics [
+ label="Analytics DB",
+ nodeRole="sink",
+ queue_size=10,
+ dropPolicy="head",
+ geometry="Box",
+ dimensions="{width: 2.0, height: 1.4, depth: 1.4}",
+ color="#7e57c2"
+ ];
+
+ // ---------------------------------------------------------------
+ // Edges — bandwidth/latency/capacity legacy attrs + DES particleSpeed
+ // + maxParticleFlow weighting (60/30/10 split downstream of LB).
+ // ---------------------------------------------------------------
+ SensorA -> LoadBalancer [label="3 p/s", particleSpeed=1.5, bandwidth=10, latency=0.05, color="#ff6b35"];
+ SensorB -> LoadBalancer [label="2 p/s", particleSpeed=1.5, bandwidth=10, latency=0.05, color="#ffa726"];
+
+ LoadBalancer -> Processor1 [label="60%", maxParticleFlow=6, particleSpeed=1.2, capacity=60, color="#66bb6a"];
+ LoadBalancer -> Processor2 [label="30%", maxParticleFlow=3, particleSpeed=1.2, capacity=30, color="#ef5350"];
+ LoadBalancer -> Processor3 [label="10%", maxParticleFlow=1, particleSpeed=1.2, capacity=10, color="#ab47bc"];
+
+ Processor1 -> Cache [particleSpeed=2.0, bandwidth=20, color="#66bb6a"];
+ Processor2 -> Cache [particleSpeed=2.0, bandwidth=20, color="#ef5350"];
+ Processor3 -> Cache [particleSpeed=2.0, bandwidth=20, color="#ab47bc"];
+
+ Cache -> Archive [label="hot path", maxParticleFlow=10, particleSpeed=2.5, bandwidth=40, color="#26c6da"];
+ Cache -> Filter [label="cold path", maxParticleFlow=4, particleSpeed=0.8, bandwidth=8, color="#ffca28", style="dashed"];
+ Filter -> Analytics [particleSpeed=1.0, bandwidth=4, color="#ffca28"];
}`,
is_public: true,
- tags: ['network', 'example', 'simple'],
- category: 'Network',
+ tags: ['showcase', 'demo', 'des', '3d', 'iot'],
+ category: 'Showcase',
is_template: true,
- template_category: 'Network Topology'
- },
- {
- title: 'Data Pipeline Simulation',
- description: 'Complex data processing pipeline with multiple stages',
- dot_code: `digraph DataPipeline {
- rankdir=TB;
-
- // Data Sources
- DataSource1 [label="Raw Data\\nIngestion", capacity=1000, processing_time=0.1, node_type="source"]
- DataSource2 [label="Stream Data\\nIngestion", capacity=500, processing_time=0.05, node_type="source"]
-
- // Processing Stages
- Cleaner [label="Data Cleaner", capacity=200, processing_time=2.0, node_type="processor"]
- Transformer [label="Transformer", capacity=150, processing_time=3.0, node_type="processor"]
- Aggregator [label="Aggregator", capacity=100, processing_time=1.5, node_type="processor"]
-
- // Storage
- DataLake [label="Data Lake", capacity=10000, processing_time=0.2, node_type="storage"]
- DataWarehouse [label="Data Warehouse", capacity=5000, processing_time=0.5, node_type="storage"]
-
- // Flows
- DataSource1 -> Cleaner [bandwidth=50, latency=0.1, data_type="raw_data"]
- DataSource2 -> Cleaner [bandwidth=25, latency=0.1, data_type="stream_data"]
- Cleaner -> Transformer [bandwidth=30, latency=0.2, data_type="clean_data"]
- Transformer -> Aggregator [bandwidth=20, latency=0.2, data_type="transformed_data"]
- Transformer -> DataLake [bandwidth=40, latency=0.1, data_type="processed_data"]
- Aggregator -> DataWarehouse [bandwidth=15, latency=0.3, data_type="aggregated_data"]
-}`,
- is_public: true,
- tags: ['data', 'pipeline', 'etl', 'complex'],
- category: 'Data Processing',
- is_template: true,
- template_category: 'Data Pipeline'
- },
- {
- title: 'Microservices Architecture',
- description: 'Microservices communication pattern with API gateway',
- dot_code: `digraph Microservices {
- rankdir=TB;
-
- // Client and Gateway
- Client [label="Client App", capacity=0, processing_time=0, node_type="client"]
- Gateway [label="API Gateway", capacity=500, processing_time=0.1, node_type="gateway"]
-
- // Services
- AuthService [label="Auth Service", capacity=100, processing_time=0.5, node_type="service"]
- UserService [label="User Service", capacity=80, processing_time=1.0, node_type="service"]
- OrderService [label="Order Service", capacity=60, processing_time=1.5, node_type="service"]
- PaymentService [label="Payment Service", capacity=40, processing_time=2.0, node_type="service"]
-
- // Databases
- AuthDB [label="Auth DB", capacity=1000, processing_time=0.1, node_type="database"]
- UserDB [label="User DB", capacity=1000, processing_time=0.1, node_type="database"]
- OrderDB [label="Order DB", capacity=800, processing_time=0.2, node_type="database"]
- PaymentDB [label="Payment DB", capacity=500, processing_time=0.3, node_type="database"]
-
- // Client flows
- Client -> Gateway [bandwidth=20, latency=0.05, data_type="api_requests"]
-
- // Gateway to services
- Gateway -> AuthService [bandwidth=5, latency=0.02, data_type="auth_requests"]
- Gateway -> UserService [bandwidth=8, latency=0.02, data_type="user_requests"]
- Gateway -> OrderService [bandwidth=6, latency=0.02, data_type="order_requests"]
- Gateway -> PaymentService [bandwidth=3, latency=0.02, data_type="payment_requests"]
-
- // Service to database flows
- AuthService -> AuthDB [bandwidth=5, latency=0.01, data_type="auth_queries"]
- UserService -> UserDB [bandwidth=8, latency=0.01, data_type="user_queries"]
- OrderService -> OrderDB [bandwidth=6, latency=0.01, data_type="order_queries"]
- PaymentService -> PaymentDB [bandwidth=3, latency=0.01, data_type="payment_queries"]
-
- // Inter-service communication
- OrderService -> UserService [bandwidth=2, latency=0.05, data_type="user_validation"]
- OrderService -> PaymentService [bandwidth=2, latency=0.05, data_type="payment_processing"]
-}`,
- is_public: true,
- tags: ['microservices', 'architecture', 'api', 'distributed'],
- category: 'Architecture',
- is_template: true,
- template_category: 'System Architecture'
+ template_category: 'Demo'
}
];
diff --git a/backend/tests/integration/routes/auth.test.js b/backend/tests/integration/routes/auth.test.js
index c74b959..002cb6f 100644
--- a/backend/tests/integration/routes/auth.test.js
+++ b/backend/tests/integration/routes/auth.test.js
@@ -198,10 +198,23 @@ describe('POST /api/auth/logout', () => {
// GET /api/auth/session
// ----------------------------------------------------------------------------
describe('GET /api/auth/session', () => {
- test('401 without session', async () => {
+ test('200 with authenticated=false when there is no session', async () => {
+ // The probe must NOT 401: the frontend hits it on every public page
+ // load (login/register) and we don't want a console error on those.
const app = buildTestApp(authRoutes, '/api/auth', { session: null });
const res = await request(app).get('/api/auth/session');
- expect(res.status).toBe(401);
+ expect(res.status).toBe(200);
+ expect(res.body).toEqual({ authenticated: false, user: null });
+ });
+
+ test('200 with authenticated=false when the user is inactive', async () => {
+ mockFindByPk.mockResolvedValue({ ...makeUser(), is_active: false });
+ const app = buildTestApp(authRoutes, '/api/auth', {
+ session: { userId: 'user-1' },
+ });
+ const res = await request(app).get('/api/auth/session');
+ expect(res.status).toBe(200);
+ expect(res.body).toEqual({ authenticated: false, user: null });
});
test('200 with the current user payload when authenticated', async () => {
diff --git a/backend/tests/unit/utils/dotValidator.test.js b/backend/tests/unit/utils/dotValidator.test.js
index 6ccd2ad..823eb80 100644
--- a/backend/tests/unit/utils/dotValidator.test.js
+++ b/backend/tests/unit/utils/dotValidator.test.js
@@ -375,4 +375,121 @@ describe('dotValidator', () => {
expect(r.metadata.hasVortexFlowExtensions).toBe(false);
});
});
+
+ // ----- DES attributes: nodeRole, dropPolicy (ADR-006) -----
+ describe('DES attributes (ADR-006)', () => {
+ describe('nodeRole', () => {
+ test.each(['generator', 'relay', 'sink'])('accepts nodeRole="%s"', async (role) => {
+ const dot = `digraph G { A [nodeRole="${role}"] B A -> B }`;
+ const r = await validator.validate(dot);
+ expect(r.valid).toBe(true);
+ expect(r.warnings.some((w) => /Invalid nodeRole/.test(w))).toBe(false);
+ });
+
+ test('warns on unknown nodeRole', async () => {
+ const dot = 'digraph G { A [nodeRole="emitter"] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(r.warnings.some((w) => /Invalid nodeRole.*emitter/.test(w))).toBe(true);
+ });
+
+ test('marks the graph as having extensions', async () => {
+ const dot = 'digraph G { A [nodeRole="generator"] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(r.metadata.hasVortexFlowExtensions).toBe(true);
+ });
+ });
+
+ describe('dropPolicy', () => {
+ test.each(['tail', 'head', 'reject'])('accepts dropPolicy="%s"', async (policy) => {
+ const dot = `digraph G { A [queue_size=10, dropPolicy="${policy}"] B A -> B }`;
+ const r = await validator.validate(dot);
+ expect(r.valid).toBe(true);
+ expect(r.warnings.some((w) => /Invalid dropPolicy/.test(w))).toBe(false);
+ });
+
+ test('warns on unknown dropPolicy', async () => {
+ const dot = 'digraph G { A [queue_size=10, dropPolicy="random"] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(r.warnings.some((w) => /Invalid dropPolicy.*random/.test(w))).toBe(true);
+ });
+
+ test('marks the graph as having extensions', async () => {
+ const dot = 'digraph G { A [queue_size=5, dropPolicy="tail"] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(r.metadata.hasVortexFlowExtensions).toBe(true);
+ });
+ });
+
+ describe('coherence warnings', () => {
+ test('warns when dropPolicy is set without queue_size', async () => {
+ const dot = 'digraph G { A [dropPolicy="tail"] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(
+ r.warnings.some((w) => /dropPolicy.*no effect.*queue_size/.test(w))
+ ).toBe(true);
+ });
+
+ test('does not warn when dropPolicy is set with queue_size', async () => {
+ const dot = 'digraph G { A [queue_size=10, dropPolicy="tail"] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(
+ r.warnings.some((w) => /dropPolicy.*no effect.*queue_size/.test(w))
+ ).toBe(false);
+ });
+
+ test('warns when particleGeneration > 0 on a relay node', async () => {
+ const dot = 'digraph G { A [nodeRole="relay", particleGeneration=5] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(
+ r.warnings.some((w) => /particleGeneration.*ignored.*nodeRole="relay"/.test(w))
+ ).toBe(true);
+ });
+
+ test('warns when particleGeneration > 0 on a sink node', async () => {
+ const dot = 'digraph G { A [nodeRole="sink", particleGeneration=5] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(
+ r.warnings.some((w) => /particleGeneration.*ignored.*nodeRole="sink"/.test(w))
+ ).toBe(true);
+ });
+
+ test('does not warn when particleGeneration > 0 on a generator node', async () => {
+ const dot = 'digraph G { A [nodeRole="generator", particleGeneration=5] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(
+ r.warnings.some((w) => /particleGeneration.*ignored/.test(w))
+ ).toBe(false);
+ });
+
+ test('does not warn when particleGeneration=0 on a relay node', async () => {
+ const dot = 'digraph G { A [nodeRole="relay", particleGeneration=0] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(
+ r.warnings.some((w) => /particleGeneration.*ignored/.test(w))
+ ).toBe(false);
+ });
+
+ test('does not warn when only nodeRole is set with no particleGeneration', async () => {
+ const dot = 'digraph G { A [nodeRole="relay"] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(
+ r.warnings.some((w) => /particleGeneration.*ignored/.test(w))
+ ).toBe(false);
+ });
+ });
+
+ describe('existing simulation attributes still validate as before', () => {
+ test('failure_rate outside [0, 1] still warns', async () => {
+ const dot = 'digraph G { A [failure_rate=1.5] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(r.warnings.some((w) => /Invalid failure_rate/.test(w))).toBe(true);
+ });
+
+ test('queue_size negative still warns', async () => {
+ const dot = 'digraph G { A [queue_size=-5] B A -> B }';
+ const r = await validator.validate(dot);
+ expect(r.warnings.some((w) => /Invalid queue_size/.test(w))).toBe(true);
+ });
+ });
+ });
});
diff --git a/backend/tests/unit/utils/setup.test.js b/backend/tests/unit/utils/setup.test.js
index b8b024c..ecbf92d 100644
--- a/backend/tests/unit/utils/setup.test.js
+++ b/backend/tests/unit/utils/setup.test.js
@@ -173,8 +173,8 @@ describe('createSampleData', () => {
await setupModule.createSampleData();
expect(Graph.create).toHaveBeenCalled();
- // The helper hardcodes 3 sample graphs.
- expect(Graph.create.mock.calls.length).toBeGreaterThanOrEqual(3);
+ // The helper now seeds a single "VortexFlow Showcase" demo graph.
+ expect(Graph.create.mock.calls.length).toBeGreaterThanOrEqual(1);
});
test('swallows errors instead of throwing (sample data is optional)', async () => {
diff --git a/doc/adr/006-particle-discrete-event-simulation.md b/doc/adr/006-particle-discrete-event-simulation.md
new file mode 100644
index 0000000..41eafbd
--- /dev/null
+++ b/doc/adr/006-particle-discrete-event-simulation.md
@@ -0,0 +1,135 @@
+# ADR-006: Particle simulation moves from continuous animation to discrete event simulation (DES)
+
+- **Status:** Accepted
+- **Date:** 2026-05-11 (Proposed) / 2026-05-12 (Accepted — implementation complete, 11 commits on `feature/des-simulation`, PR #30)
+- **Tags:** frontend, simulation, dot, renderer, performance
+
+## Context
+
+VortexFlow currently renders particles via `3d-force-graph`'s
+`linkDirectionalParticles(link)`, which paints a fixed number of particles
+continuously circulating along each link as long as the simulation is
+running. This produces a pleasing animation but has structural limits:
+
+- Each link animates **independently**. There is no notion of a particle
+ arriving at a node and being routed onward.
+- Nodes cannot **accumulate** particles. A bottleneck (1 input ≫ 1 output)
+ is not visible — every link displays its own steady flow.
+- The DOT 3D spec defines `queue_size`, `processing_time`, `failure_rate`,
+ `maxParticleProcessing` (per node) and `maxParticleFlow` (per link),
+ but the runtime ignores them. They live only in the validator.
+- `simulationStats.totalParticles / averageLatency / bottleneckNodes`
+ are derived heuristically (in-degree, scene particle count) rather
+ than measured. They look right, but they don't _mean_ anything.
+- The one-shot trace (`handleEmitTrace`) already approximates a discrete
+ cascade — it tracks particles per node via `setTimeout` and a visited
+ set — but it's a separate code path that bypasses the continuous mode.
+
+We need a simulation model where:
+
+1. Specific nodes **emit** particles at a defined rate.
+2. Particles **transit** along links at link-specific speeds.
+3. Nodes receive particles into a **queue**, optionally bounded.
+4. Queues that overflow **drop** particles according to a configurable
+ policy.
+5. The renderer visualizes accumulation (node grows) and saturation
+ (flash / counter on drop).
+
+## Decision
+
+Replace the continuous animation model with a **discrete event simulation
+(DES)** executed in the browser:
+
+- A new `ParticleSimulator` service owns the logical state (particles in
+ transit, per-node queues, per-node stats).
+- The simulator advances on every `requestAnimationFrame`, with a clamped
+ `dt` to tolerate background-tab throttling.
+- `3d-force-graph` is kept as the **renderer**, but is driven by the
+ simulator: `emitParticle(link)` is called whenever the simulator
+ releases a particle onto a link. `linkDirectionalParticles(link)`
+ returns 0 — continuous animation is off.
+- The DOT 3D surface gains two new attributes (see ADR-005 triple invariant):
+ - **`nodeRole`** (enum: `generator | relay | sink`, default `relay`) —
+ role-based emission. Only `generator` nodes spawn particles. `sink`
+ nodes absorb particles without further routing. **V1 is strict**:
+ no implicit fallback "everyone emits" — graphs without `nodeRole`
+ will not animate until annotated.
+ - **`dropPolicy`** (enum: `tail | head | reject`, default `tail`) —
+ behaviour when a particle arrives at a node whose queue is full.
+ Only meaningful when `queue_size` is also defined.
+- Existing attributes (`particleGeneration`, `maxParticleProcessing`,
+ `queue_size`, `processing_time`, `failure_rate`, `maxParticleFlow`,
+ `particleSpeed`) keep their names and gain real runtime semantics.
+
+Concretely, this means:
+
+- The simulator is the **single source of truth** for `totalParticles`,
+ `averageLatency`, `bottleneckNodes`, and a new `droppedCount`.
+- Nodes visually grow with queue occupancy (capped at 2× base size).
+- A drop triggers a brief red flash plus an incrementing on-node counter.
+- The one-shot `handleEmitTrace` is realigned: it respects `nodeRole`
+ too, so the one-shot button always fires from the same set of
+ generators that the continuous simulation does.
+- Routing at a node with M outgoing links: weighted by `maxParticleFlow`,
+ with round-robin as fallback when no weights are defined.
+- Default values when an attribute is missing on a node where it would
+ matter:
+ - `nodeRole=generator` without `particleGeneration` → 1 particle/sec
+ - `queue_size` undefined → unbounded queue (no drops)
+ - `dropPolicy` undefined → `tail` (drops the incoming particle)
+ - `failure_rate` undefined → 0 (no random drops)
+
+## Consequences
+
+Positive:
+
+- **Semantics finally match the spec**. The DOT 3D attributes that
+ already existed but were ignored at runtime now have a behaviour.
+- **Real metrics**: queue sizes, drops, throughput, latency are measured,
+ not guessed. The HUD stats become meaningful.
+- **Bottleneck visualization**: visible immediately on overload, not
+ inferred from topology.
+- **Convergence and divergence look right**: M inputs → 1 node → 1 output
+ shows accumulation; 1 input → 1 node → M outputs shows distribution
+ weighted by `maxParticleFlow`.
+- **One source of decision** for who emits (single rule in the simulator
+ consumed by both continuous and one-shot paths).
+
+Negative:
+
+- **Breaking change for existing graphs**: graphs without `nodeRole`
+ will not emit. They must be annotated. We chose strict V1 over
+ silent fallback to keep the model honest. Migration cost is real but
+ contained — `nodeRole=generator` is a one-line addition per emitter.
+- **CPU cost**: a JS loop that advances N particles 60 times per second
+ is heavier than letting WebGL animate fixed-rate streams. Mitigations
+ (RAF throttle, optional Web Worker) listed in Phase 7 of the
+ implementation plan.
+- **Renderer file is already ~1600 lines**. The integration adds a hook
+ (`useParticleSimulator`) and removes the `linkDirectionalParticles`
+ branch, so the net delta should be neutral, but the file is fragile.
+- **Three-place invariant (ADR-005) must be honoured** for two new
+ attributes. Triple update validated in Phase 1.
+
+## Alternatives considered
+
+- **Option A — visual-only indicators** (halo / size based on theoretical
+ imbalance, without changing the simulation). Cheap (~0.5d) and pretty,
+ but does not actually simulate accumulation or drops — the spec
+ attributes stay ignored, the stats stay heuristic. Rejected as a
+ long-term direction.
+- **Option B — modulated continuous flow** (adjust
+ `linkDirectionalParticles` dynamically based on graph topology).
+ Better than A but still statistical: individual particles don't
+ travel — the visual is a distribution. Cannot model `queue_size`,
+ `processing_time`, `failure_rate`. Rejected as the target, but
+ acceptable as a stopgap if DES schedule slips.
+- **Server-driven simulation** (re-activate `routes/simulation.js`).
+ Rejected: ADR-002 already decided to run simulation in the browser
+ for latency and offline support. Re-opening that decision is out of
+ scope.
+- **Implicit fallback "everyone emits" when `nodeRole` is missing**.
+ Considered for backwards-compatibility, rejected. The rule must be
+ observable from the graph source; magic defaults make graphs behave
+ differently depending on what other attributes are present, which
+ is exactly the kind of surprise we want to avoid.
diff --git a/doc/adr/README.md b/doc/adr/README.md
index 6aed813..1267b29 100644
--- a/doc/adr/README.md
+++ b/doc/adr/README.md
@@ -17,13 +17,14 @@ We use a lightweight MADR-inspired format:
## Index
-| # | Title | Status |
-|---|---|---|
-| [001](./001-redis-session-store.md) | Use Redis-backed `express-session` instead of JWTs | Accepted |
-| [002](./002-browser-side-simulation.md) | Run particle simulation entirely in the browser | Accepted |
-| [003](./003-vite-over-cra.md) | Migrate frontend build from CRA to Vite | Accepted |
-| [004](./004-sequelize-sync-vs-migrations.md) | Use `sequelize.sync({alter})` in dev; migrations as a planned remediation | Transitional |
-| [005](./005-dot-3d-triple-invariant.md) | DOT 3D extensions live in three places that must stay in sync | Accepted |
+| # | Title | Status |
+| -------------------------------------------------- | ------------------------------------------------------------------------- | ------------ |
+| [001](./001-redis-session-store.md) | Use Redis-backed `express-session` instead of JWTs | Accepted |
+| [002](./002-browser-side-simulation.md) | Run particle simulation entirely in the browser | Accepted |
+| [003](./003-vite-over-cra.md) | Migrate frontend build from CRA to Vite | Accepted |
+| [004](./004-sequelize-sync-vs-migrations.md) | Use `sequelize.sync({alter})` in dev; migrations as a planned remediation | Transitional |
+| [005](./005-dot-3d-triple-invariant.md) | DOT 3D extensions live in three places that must stay in sync | Accepted |
+| [006](./006-particle-discrete-event-simulation.md) | Particle simulation moves from continuous animation to DES | Accepted |
## Writing a new ADR
diff --git a/doc/changelog/2026-05-12.md b/doc/changelog/2026-05-12.md
new file mode 100644
index 0000000..a95cd1d
--- /dev/null
+++ b/doc/changelog/2026-05-12.md
@@ -0,0 +1,116 @@
+# Changelog - 2026-05-12
+
+## Summary
+
+Suite de la session du 2026-05-11 sur le chantier **Discrete Event
+Simulation (DES)** des particules (ADR-006). Phases 3 → 8 finalisées
+sur la branche `feature/des-simulation` (PR #30, 12 commits au total).
+
+Le chantier remplace le rendu continu de `linkDirectionalParticles`
+par un simulator événementiel piloté par les attributs DOT
+(`nodeRole`, `particleGeneration`, `queue_size`, `processing_time`,
+`failure_rate`, `dropPolicy`, `maxParticleFlow`). L'utilisateur
+voit désormais l'accumulation (grossissement des nœuds), la
+saturation (halo orange/rouge), les drops (flash rouge + compteur)
+et les métriques temps réel dans le HUD.
+
+## Changes
+
+- [doc/adr/006-particle-discrete-event-simulation.md] Statut
+ passé de **Proposed** à **Accepted** (chantier livré).
+- [doc/adr/README.md] Index mis à jour avec le nouveau statut.
+- [doc/dot-3d/user-guide.md] Nouvel "Exemple 0 : Pipeline DES avec
+ goulot et drops" ajouté à la section "Exemples Pratiques Complets".
+ Documente le pipeline source→goulot→sink avec accumulation, halos
+ et drops, et propose 3 variantes (slots multiples, failure_rate,
+ convergence).
+- [frontend/src/services/particleSimulator.ts] (créé en Phase 2,
+ implémenté en Phase 3) Classe DES pure, 430 lignes. Gère émission
+ régulière depuis les générateurs, transit sur les liens avec
+ calibration alignée sur `handleEmitTrace`, queues bornées avec
+ 3 politiques de drop, slots parallèles avec processing_time,
+ failure_rate à la sortie, routing pondéré par maxParticleFlow,
+ lifecycle start/pause/stop/dispose, stats temps réel via onTick.
+- [frontend/src/services/particleSimulator.test.ts] 37 tests
+ unitaires couvrant chaque branche métier. Coverage 97.11% lines /
+ 94.62% branches / 100% functions / 98.44% statements.
+- [frontend/src/services/particleSimulator.integration.test.ts]
+ 6 tests end-to-end : convergence (3→1→1), divergence pondérée
+ (50/30/20), cycle (A→B→C→B), saturation `tail`, saturation
+ `head`, perf smoke 100 nœuds / 100 ticks < 500ms.
+- [frontend/src/hooks/useParticleSimulator.ts] Hook React qui
+ owns une instance de simulator, la pilote via rAF, surface les
+ stats via useState, forward onParticleReleased au callback du
+ caller. ~145 lignes, 100% lines / 75% branches.
+- [frontend/src/hooks/useParticleSimulator.test.ts] 9 tests
+ avec stub rAF déterministe pour reproductibilité.
+- [frontend/src/components/graphs/GraphRenderer3D.tsx] Intégration
+ end-to-end : hook câblé, accesseurs nodeVal/nodeColor étendus pour
+ le grossissement / halo / flash / role tint, useEffect dédié aux
+ refs visuelles, fallback heuristique préservé pour graphes legacy,
+ handleEmitTrace aligné sur la règle stricte (nodeRole='generator'
+ uniquement), HUD enrichi avec Drops + File max + Débit + tooltips,
+ reset session-scoped sur le edge start→start.
+- [frontend/src/components/graphs/GraphRenderer3D.test.tsx]
+ SAMPLE_PARSE.A reçoit nodeRole='generator', test
+ linkDirectionalParticles renommé pour vérifier le mode DES (return 0
+ - emitParticle est appelée).
+- [frontend/src/components/graphs/GraphViewer.tsx] Nettoyage :
+ handleToggleSimulation supprimée (dead code suite à la suppression
+ du bouton play du rail en main).
+- [backend/src/routes/public.js] parse-dot inclut maintenant
+ nodeRole, dropPolicy, queue_size, processing_time, failure_rate
+ dans la réponse pour que le ParticleSimulator les reçoive.
+- [backend/src/utils/dotValidator.js] (Phase 1) Reconnaît nodeRole
+ et dropPolicy via deux nouveaux Sets validation. Méthode
+ validateDESCoherence() qui warn sur dropPolicy sans queue_size et
+ particleGeneration > 0 sur relay/sink. Coverage 86.91%.
+- [backend/tests/unit/utils/dotValidator.test.js] 17 nouveaux tests
+ pour DES : 3 valeurs nodeRole, 3 valeurs dropPolicy, valeurs hors
+ énum, 7 cas de cohérence inter-attributs, 2 régressions.
+- [frontend/vite.config.ts] Coverage threshold per-module pour
+ particleSimulator.ts (90/85/95/90) pour locker contre les
+ régressions futures.
+- [doc/dot-3d/grammar-specification.md] Section 1.2 enrichie avec
+ les 7 attributs DES + table des rôles. Section 7.1 reçoit les
+ contraintes logiques.
+- [doc/dot-3d/bnf-grammar.md] Productions BNF pour node_role et
+ drop_policy + warning rules de cohérence.
+- [doc/dot-3d/validation-rules.md] Validators TypeScript pour
+ les 7 attributs et rules de cohérence inter-attributs.
+- [doc/dot-3d/examples/generators.dot] (créé Phase 0) Démo
+ minimale des 3 rôles, pas de saturation.
+- [doc/dot-3d/examples/saturation.dot] (créé Phase 0) Source rapide
+ → goulot étroit → sink, pour valider visuellement les phases 5/7.
+- [doc/dot-3d/examples/README.md] Index mis à jour avec les
+ 2 nouveaux exemples.
+- [frontend/doc/RENDERER.md] Section 8.bis "DES visual hooks"
+ documentée : queue growth, saturation halo, drop flash, role tint,
+ pattern refs-vs-state, re-install des accesseurs.
+
+## Tests
+
+- Backend : 395 tests verts (376 historiques + 17 DES + 2 autres
+ ajustements en cours de chantier). Coverage globale conforme aux
+ thresholds (lines 70 / branches 60 / functions 68 / statements 70).
+- Frontend : 360 tests verts (320 historiques + 37 simulator unit +
+ 6 integration + 9 hook + adaptations). Coverage : particleSimulator
+ 97.11% lines, useParticleSimulator 100% lines. Lint clean.
+- CI GitHub Actions sur Node 24 : à valider sur la PR #30 après merge.
+
+## Notes
+
+- Le simulator tourne uniquement quand au moins un nœud déclare
+ `nodeRole="generator"` (V1 stricte, ADR-006). Les graphes legacy
+ sans annotation continuent de fonctionner via le fallback
+ continuous-flow de `3d-force-graph`. Migration optionnelle.
+- `handleEmitTrace` (le bouton ⚡ "Émission particules") respecte
+ désormais la même règle stricte : il ne fait rien si aucun
+ générateur n'est déclaré.
+- Pas de cap sur le nombre total de particules en vol (decision
+ Phase 3 — D9). Si la perf souffre sur très gros graphes,
+ l'option `maxTotalParticlesInFlight` peut être ajoutée à
+ `SimulatorOptions` sans breaking change.
+- Tests renderer DOM pour les chips HUD (Drops / File max / Débit)
+ intentionnellement non écrits : ils dépendent du rAF réel, et
+ chaque maillon de la chaîne est testé séparément.
diff --git a/doc/changelog/2026-05-13.md b/doc/changelog/2026-05-13.md
new file mode 100644
index 0000000..5795e2d
--- /dev/null
+++ b/doc/changelog/2026-05-13.md
@@ -0,0 +1,20 @@
+# Changelog - 2026-05-13
+
+## Summary
+
+Replaced the three legacy seed graphs (Simple Network Flow, Data Pipeline,
+Microservices Architecture) with a single "VortexFlow Showcase" demo graph
+that exercises every documented feature of the renderer.
+
+## Changes
+
+- [backend/src/utils/setup.js] Replaced `sampleGraphs` triple with a single demo graph (10 nodes, 13 edges) covering all 3 nodeRoles (generator/relay/sink), all 5 3D geometries (Sphere/Box/Cylinder/Cone/Torus), both dropPolicies (tail/head), failure_rate, weighted maxParticleFlow routing (60/30/10 dispatch), variable particleSpeed, queue saturation and legacy bandwidth/capacity/latency attributes.
+- [backend/tests/unit/utils/setup.test.js] Adjusted Graph.create call-count assertion from `>= 3` to `>= 1` and updated the inline comment accordingly.
+- [frontend/src/components/graphs/GraphList.tsx] Fixed the Supprimer / Dupliquer buttons in the per-card kebab menu: they were calling `handleMenuClose()` (which nulls `selectedGraphId`) before opening the confirmation dialog, so the dialog's action button became a no-op. Close only the menu anchor now and let the dialog read the live id.
+- [frontend/src/components/graphs/GraphList.test.tsx] Added 2 regression tests covering the Supprimer and Dupliquer end-to-end flows.
+
+## Notes
+
+The seed only runs when the admin user has zero graphs in DB. To see the new
+demo on an existing install, delete the admin's existing graphs first or
+restart against a fresh database.
diff --git a/doc/dot-3d/bnf-grammar.md b/doc/dot-3d/bnf-grammar.md
index a9e4688..dfab90e 100644
--- a/doc/dot-3d/bnf-grammar.md
+++ b/doc/dot-3d/bnf-grammar.md
@@ -10,9 +10,9 @@ graph ::= ['strict'] ('graph' | 'digraph') [id] '{' stmt_list '}'
stmt_list ::= [stmt [';' | '\n'] stmt_list]
-stmt ::= node_stmt
- | edge_stmt
- | attr_stmt
+stmt ::= node_stmt
+ | edge_stmt
+ | attr_stmt
| id '=' id
| subgraph
| global_3d_config
@@ -48,8 +48,16 @@ visual_3d_attribute ::= 'geometry' '=' geometry_type
| 'dimensions' '=' dimensions_object
| 'image' '=' url_string
-simulation_attribute ::= 'particleGeneration' '=' number
+simulation_attribute ::= 'nodeRole' '=' node_role
+ | 'particleGeneration' '=' number
| 'maxParticleProcessing' '=' number
+ | 'queue_size' '=' integer
+ | 'processing_time' '=' number
+ | 'failure_rate' '=' number
+ | 'dropPolicy' '=' drop_policy
+
+node_role ::= '"generator"' | '"relay"' | '"sink"'
+drop_policy ::= '"tail"' | '"head"' | '"reject"'
(* === TYPES GÉOMÉTRIQUES === *)
geometry_type ::= '"Sphere"' | '"Box"' | '"Cylinder"' | '"Cone"' | '"Torus"'
@@ -59,7 +67,7 @@ dimension_list ::= dimension (',' dimension)*
dimension ::= 'radius' ':' number
| 'width' ':' number
- | 'height' ':' number
+ | 'height' ':' number
| 'depth' ':' number
| 'tube' ':' number
| 'tubularSegments' ':' integer
@@ -110,42 +118,57 @@ attr_stmt ::= ('graph' | 'node' | 'edge') attr_list
```ebnf
(* Contraintes à valider après parsing *)
-constraint ::= geometric_constraint
- | simulation_constraint
+constraint ::= geometric_constraint
+ | simulation_constraint
| performance_constraint
-geometric_constraint ::= sphere_constraint
- | box_constraint
- | cylinder_constraint
- | cone_constraint
+geometric_constraint ::= sphere_constraint
+ | box_constraint
+ | cylinder_constraint
+ | cone_constraint
| torus_constraint
-sphere_constraint ::=
- WHEN geometry="Sphere"
+sphere_constraint ::=
+ WHEN geometry="Sphere"
THEN radius > 0
-box_constraint ::=
- WHEN geometry="Box"
+box_constraint ::=
+ WHEN geometry="Box"
THEN width > 0 AND height > 0 AND depth > 0
-cylinder_constraint ::=
- WHEN geometry="Cylinder"
+cylinder_constraint ::=
+ WHEN geometry="Cylinder"
THEN radius > 0 AND height > 0
-cone_constraint ::=
- WHEN geometry="Cone"
+cone_constraint ::=
+ WHEN geometry="Cone"
THEN radius > 0 AND height > 0
-torus_constraint ::=
- WHEN geometry="Torus"
+torus_constraint ::=
+ WHEN geometry="Torus"
THEN tube > 0 AND tubularSegments >= 3 AND radialSegments >= 3
-simulation_constraint ::=
+simulation_constraint ::=
particleGeneration >= 0 AND
maxParticleProcessing > 0 AND
maxParticleFlow > 0 AND
particleSpeed > 0 AND
- defaultNodeSize > 0
+ defaultNodeSize > 0 AND
+ queue_size > 0 AND
+ processing_time >= 0 AND
+ failure_rate >= 0 AND failure_rate <= 1
+
+role_constraint ::=
+ nodeRole ∈ {"generator", "relay", "sink"} AND
+ dropPolicy ∈ {"tail", "head", "reject"}
+
+(* Cross-attribute coherence — warnings, not errors *)
+coherence_warning ::=
+ WHEN dropPolicy DEFINED AND queue_size NOT DEFINED
+ THEN warn("dropPolicy is ignored without queue_size")
+
+ WHEN particleGeneration > 0 AND nodeRole IN {"relay", "sink"}
+ THEN warn("particleGeneration is ignored unless nodeRole=generator")
```
## Exemples de Productions
@@ -170,7 +193,7 @@ node_stmt:
```
edge_stmt:
node_id: "ServerA"
- edgeop: "->"
+ edgeop: "->"
node_id: "ServerB"
attr_list: [
edge_3d_attribute: maxParticleFlow=45
@@ -204,14 +227,14 @@ global_3d_config:
### Gestion des Conflits
```ebnf
-conflict_resolution ::=
+conflict_resolution ::=
WHEN duplicate_attribute
THEN use_last_defined
-
+
WHEN global_vs_local
THEN prefer_local
-
- WHEN invalid_combination
+
+ WHEN invalid_combination
THEN emit_error_with_suggestion
```
diff --git a/doc/dot-3d/examples/README.md b/doc/dot-3d/examples/README.md
index 90270cf..4bb83bc 100644
--- a/doc/dot-3d/examples/README.md
+++ b/doc/dot-3d/examples/README.md
@@ -4,8 +4,26 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
## 📋 Liste des Exemples
+### 0. 🎯 [Rôles `nodeRole`](./generators.dot) _(DES — ADR-006)_
+
+**Cas d'usage** : démonstration minimale des trois rôles (`generator`, `relay`, `sink`).
+
+- **Topologie** : 2 sources convergent vers 1 routeur vers 1 puits.
+- **Pas de saturation** — le routeur a la capacité de tout traiter.
+- **À utiliser pour** : comprendre l'effet de `nodeRole` avant de plonger dans des cas plus riches.
+
+### 0.bis. 🚨 [Goulot avec drop](./saturation.dot) _(DES — ADR-006)_
+
+**Cas d'usage** : démonstration de l'accumulation, du halo de saturation et des drops visibles.
+
+- **Topologie** : 1 source rapide → 1 goulot étroit → 1 puits.
+- **Comportement** : la file du goulot se remplit, halo orange à 80 %, drops dès saturation, flash rouge à chaque drop.
+- **À utiliser pour** : valider les phases 5 (visu) et 7 (test d'intégration "saturation") du chantier DES.
+
### 1. 🌐 [Réseau de Distribution](./network-distribution.dot)
+
**Cas d'usage** : Infrastructure réseau avec flux de données
+
- **Domaine** : Télécommunications, Cloud Computing
- **Géométries** : Box (datacenters), Cylinder (serveurs régionaux), Cone (edge), Torus (CDN), Sphere (utilisateurs)
- **Particules** : Représentent les requêtes et données transitant
@@ -13,13 +31,16 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
- **Vitesses** : Variables selon la géographie (backbone transpacifique plus lent)
**Points d'intérêt** :
+
- Accumulation visible aux goulots d'étranglement
- Différentes vitesses selon la latence géographique
- Flux bidirectionnel avec analytics de retour
- CDN optimisé pour haute performance
### 2. ⚛️ [Physique des Particules](./particle-physics.dot)
+
**Cas d'usage** : Accélérateur de particules et détection
+
- **Domaine** : Recherche scientifique, Physique
- **Géométries** : Sphere (sources), Cylinder (accélérateurs), Torus (cyclotrons), Box (détecteurs)
- **Particules** : Protons, électrons et particules secondaires
@@ -27,13 +48,16 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
- **Vitesses** : Accélération progressive, très haute énergie avant collision
**Points d'intérêt** :
+
- Génération de particules secondaires après collision
- Pertes par rayonnement dans les cyclotrons
- Détection multi-canal parallèle
- Boucles de rétroaction pour calibration
### 3. 🔄 [Pipeline de Workflow](./workflow-pipeline.dot)
+
**Cas d'usage** : CI/CD avec goulots d'étranglement
+
- **Domaine** : DevOps, Développement logiciel
- **Géométries** : Box (repos, compilation), Cone (tests), Cylinder (intégration), Torus (sécurité)
- **Particules** : Commits, builds, et artefacts
@@ -41,13 +65,16 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
- **Vitesses** : Hotfix rapides, déploiement production contrôlé
**Points d'intérêt** :
+
- Parallélisation des tests après compilation
- Synchronisation avant packaging Docker
- Chemin hotfix contournant certaines étapes
- Feedback d'erreurs vers développement
### 4. 📱 [Réseau Social](./social-network.dot)
+
**Cas d'usage** : Propagation virale de contenu
+
- **Domaine** : Réseaux sociaux, Marketing viral
- **Géométries** : Sphere (influenceurs, audience), Cylinder (comptes), Torus (communautés), Box (algorithmes)
- **Particules** : Posts, shares, engagements
@@ -55,13 +82,16 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
- **Vitesses** : Contenu lifestyle plus viral que tech
**Points d'intérêt** :
+
- Amplification algorithmique automatique
- Croisements entre communautés thématiques
- Boucles de rétroaction d'engagement
- Système de tendances détectant la viralité
### 5. 💰 [Système Économique](./economic-system.dot)
+
**Cas d'usage** : Flux financiers et monétaires
+
- **Domaine** : Économie, Finance, Politique monétaire
- **Géométries** : Sphere (banque centrale, ménages), Cylinder (banques), Box (industries, état), Torus (marchés)
- **Particules** : Monnaie, crédits, investissements
@@ -69,6 +99,7 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
- **Vitesses** : Transactions rapides vs procédures administratives lentes
**Points d'intérêt** :
+
- Circuit monétaire complet banque centrale → économie
- Chaîne de valeur primaire → secondaire → tertiaire
- Circuit fiscal collecte → redistribution
@@ -77,6 +108,7 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
## 🎯 Concepts Clés Démontrés
### **Géométries 3D Avancées**
+
- **Sphere** : Entités centrales, sources/destinataires
- **Box** : Infrastructures, centres de traitement
- **Cylinder** : Relais, serveurs, institutions
@@ -84,12 +116,14 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
- **Torus** : Systèmes circulaires, algorithmes, marchés
### **Simulation de Particules**
+
- **Génération** : `particleGeneration` - Production de contenu/flux
- **Traitement** : `maxParticleProcessing` - Capacité de traitement
- **Goulots** : Accumulation quand génération > traitement
- **Vitesses** : `particleSpeed` - Urgence et priorité des flux
### **Effets Visuels**
+
- **Auto-resize** : Taille des nœuds selon leur importance
- **Bloom effect** : Halo lumineux sur les accumulations
- **Couleurs thématiques** : Codage par secteur/fonction
@@ -98,18 +132,21 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
## 🚀 Utilisation dans VortexFlow
### **Chargement des Exemples**
+
1. Copiez le contenu d'un fichier `.dot`
2. Collez dans l'éditeur VortexFlow
3. Cliquez sur "Visualiser en 3D"
4. Activez la simulation de particules dans le panneau de contrôle
### **Contrôles Interactifs**
+
- **Particules** : Activez/désactivez avec le bouton toggle
- **Vitesse** : Ajustez la vitesse globale de simulation
- **Bloom** : Intensifiez l'effet lumineux sur les accumulations
- **Tailles** : Utilisez auto-resize pour voir l'importance relative
### **Personnalisation**
+
- **Couleurs** : Modifiez les attributs `color` pour votre thème
- **Géométries** : Changez `geometry` et `dimensions` selon vos besoins
- **Flux** : Ajustez `maxParticleFlow` et `particleSpeed` pour votre cas
@@ -118,6 +155,7 @@ Cette collection d'exemples démontre les capacités avancées de l'extension DO
## 🔧 Paramètres Recommandés
### **Pour Démonstrations**
+
```dot
defaultNodeSize = 1.2;
particlesEnabled = true;
@@ -126,6 +164,7 @@ bloomEffect = true;
```
### **Pour Analyses Détaillées**
+
```dot
defaultNodeSize = 0.8;
particlesEnabled = true;
@@ -134,6 +173,7 @@ bloomEffect = false; // Moins de distraction
```
### **Pour Présentations**
+
```dot
defaultNodeSize = 1.5;
particlesEnabled = true;
@@ -155,16 +195,19 @@ Ces exemples permettent de visualiser des métriques importantes :
## 🎓 Cas d'Usage Étendus
### **Infrastructure IT**
+
- Monitoring réseau avec alertes visuelles
- Analyse de performance des microservices
- Visualisation des flux de données en temps réel
### **Analyse Métier**
+
- Flux de processus avec identification des goulots
- Propagation d'information dans l'organisation
- Modélisation des chaînes d'approvisionnement
### **Recherche et Éducation**
+
- Simulation de systèmes complexes
- Visualisation de modèles théoriques
- Démonstrations interactives pour l'enseignement
diff --git a/doc/dot-3d/examples/generators.dot b/doc/dot-3d/examples/generators.dot
new file mode 100644
index 0000000..56b3788
--- /dev/null
+++ b/doc/dot-3d/examples/generators.dot
@@ -0,0 +1,65 @@
+// generators.dot — Exemple démontrant les trois rôles nodeRole.
+//
+// Topologie :
+// SourceA ─┐
+// ├─→ Router ─→ Sink
+// SourceB ─┘
+//
+// Deux générateurs convergent vers un relay (Router) qui répartit vers
+// un sink (Drain). Le sink absorbe sans rerouter — la particule disparaît
+// à son arrivée.
+//
+// Ce graphe ne sature pas : SourceA + SourceB = 3 p/s entrent dans Router,
+// qui traite 10 p/s. Pas de drop, pas d'accumulation visible.
+// Voir saturation.dot pour le cas inverse.
+
+digraph Generators {
+ // Configuration globale
+ defaultNodeSize = 1.5;
+ particlesEnabled = true;
+ autoColors = false;
+
+ // Deux sources actives
+ SourceA [
+ label="Source A",
+ nodeRole="generator",
+ particleGeneration=2, // 2 particules/seconde
+ geometry="Cone",
+ color="#ff6666"
+ ];
+
+ SourceB [
+ label="Source B",
+ nodeRole="generator",
+ particleGeneration=1, // 1 particule/seconde
+ geometry="Cone",
+ color="#ff9966"
+ ];
+
+ // Routeur — reçoit, traite, redistribue
+ Router [
+ label="Routeur central",
+ // nodeRole omis → relay par défaut
+ maxParticleProcessing=10,
+ queue_size=30,
+ dropPolicy="tail",
+ processing_time=2,
+ geometry="Box",
+ dimensions="{width: 2.0, height: 1.5, depth: 1.5}",
+ color="#66cc66"
+ ];
+
+ // Puits — absorbe sans rerouter
+ Sink [
+ label="Drain",
+ nodeRole="sink",
+ geometry="Sphere",
+ dimensions="{radius: 1.2}",
+ color="#666699"
+ ];
+
+ // Connexions
+ SourceA -> Router [maxParticleFlow=5, particleSpeed=1.5, color="#ff6666"];
+ SourceB -> Router [maxParticleFlow=5, particleSpeed=1.5, color="#ff9966"];
+ Router -> Sink [maxParticleFlow=15, particleSpeed=1.0, color="#66cc66"];
+}
diff --git a/doc/dot-3d/examples/saturation.dot b/doc/dot-3d/examples/saturation.dot
new file mode 100644
index 0000000..4068f7d
--- /dev/null
+++ b/doc/dot-3d/examples/saturation.dot
@@ -0,0 +1,67 @@
+// saturation.dot — Exemple démontrant l'accumulation puis le drop.
+//
+// Topologie :
+// FastSource ─→ Bottleneck ─→ Sink
+//
+// FastSource génère 10 particules/seconde, mais Bottleneck ne peut en
+// traiter que 2/seconde et sa file ne stocke que 5 particules.
+//
+// Déroulement attendu à l'écran :
+// 1. Les particules arrivent plus vite qu'elles ne sortent.
+// 2. Bottleneck grossit visuellement à mesure que sa file se remplit.
+// 3. À 80 % de remplissage, halo orange.
+// 4. Quand la file est pleine (5/5), nouvelle arrivée → drop : flash
+// rouge + compteur de drops incrémente.
+// 5. dropPolicy="tail" : c'est la particule entrante qui est jetée
+// (changer en "head" pour drop la plus ancienne à la place).
+//
+// Utile pour valider la phase 5 (visualisation) et la phase 7 (test
+// d'intégration "saturation").
+
+digraph Saturation {
+ defaultNodeSize = 1.2;
+ particlesEnabled = true;
+ autoColors = false;
+
+ FastSource [
+ label="Source rapide",
+ nodeRole="generator",
+ particleGeneration=10, // 10 p/s — bien au-dessus de la capacité aval
+ geometry="Cone",
+ dimensions="{radius: 1.0, height: 2.0}",
+ color="#ff4444"
+ ];
+
+ Bottleneck [
+ label="Goulot",
+ nodeRole="relay",
+ maxParticleProcessing=2, // 2 p/s — capacité limitée
+ queue_size=5, // File minuscule
+ dropPolicy="tail", // Drop l'entrante quand pleine
+ processing_time=400, // 400 ms par particule → renforce le goulot
+ failure_rate=0, // Pas d'échec aléatoire à la sortie
+ geometry="Cylinder",
+ dimensions="{radius: 1.0, height: 2.0}",
+ color="#ffaa00"
+ ];
+
+ Sink [
+ label="Sortie",
+ nodeRole="sink",
+ geometry="Sphere",
+ dimensions="{radius: 1.0}",
+ color="#4477aa"
+ ];
+
+ FastSource -> Bottleneck [
+ maxParticleFlow=20,
+ particleSpeed=2.0, // Vitesse rapide → aggrave le goulot
+ color="#ff4444"
+ ];
+
+ Bottleneck -> Sink [
+ maxParticleFlow=10,
+ particleSpeed=1.0,
+ color="#ffaa00"
+ ];
+}
diff --git a/doc/dot-3d/grammar-specification.md b/doc/dot-3d/grammar-specification.md
index 9e2858a..f862e4d 100644
--- a/doc/dot-3d/grammar-specification.md
+++ b/doc/dot-3d/grammar-specification.md
@@ -8,7 +8,7 @@ Cette spécification définit l'extension de la grammaire DOT standard pour supp
✅ **Compatibilité descendante complète** avec la syntaxe DOT standard
✅ **Extensions non-intrusives** via nouveaux attributs optionnels
-✅ **Validation progressive** pour maintenir la robustesse
+✅ **Validation progressive** pour maintenir la robustesse
---
@@ -16,47 +16,70 @@ Cette spécification définit l'extension de la grammaire DOT standard pour supp
### 1.1 Attributs Visuels
-| Attribut | Type | Défaut | Description |
-|----------|------|--------|-------------|
-| `label` | string | ID du nœud | Texte affiché sur le nœud |
-| `color` | color | auto | Couleur au format hex (#ff0000), rgb(255,0,0) ou nom (red) |
-| `image` | url | null | URL d'une image à appliquer comme texture |
-| `geometry` | enum | "Sphere" | Type de géométrie 3D |
-| `dimensions` | object | géométrie-dépendant | Paramètres dimensionnels |
+| Attribut | Type | Défaut | Description |
+| ------------ | ------ | ------------------- | ---------------------------------------------------------- |
+| `label` | string | ID du nœud | Texte affiché sur le nœud |
+| `color` | color | auto | Couleur au format hex (#ff0000), rgb(255,0,0) ou nom (red) |
+| `image` | url | null | URL d'une image à appliquer comme texture |
+| `geometry` | enum | "Sphere" | Type de géométrie 3D |
+| `dimensions` | object | géométrie-dépendant | Paramètres dimensionnels |
#### Géométries 3D Supportées
**Sphere** (défaut)
+
```dot
A [geometry="Sphere", dimensions="{radius: 1.0}"];
```
**Box**
+
```dot
B [geometry="Box", dimensions="{width: 2.0, height: 1.5, depth: 1.0}"];
```
**Cylinder**
+
```dot
C [geometry="Cylinder", dimensions="{radius: 0.8, height: 2.0}"];
```
**Cone**
+
```dot
D [geometry="Cone", dimensions="{radius: 1.0, height: 2.5}"];
```
**Torus**
+
```dot
E [geometry="Torus", dimensions="{tube: 0.3, tubularSegments: 8, radialSegments: 6}"];
```
### 1.2 Attributs de Simulation
-| Attribut | Type | Défaut | Description |
-|----------|------|--------|-------------|
-| `particleGeneration` | number | 0 | Particules générées par minute |
-| `maxParticleProcessing` | number | 60 | Particules maximum traitées par minute |
+| Attribut | Type | Défaut | Description |
+| ----------------------- | ------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `nodeRole` | enum | `relay` | Rôle du nœud dans la simulation (`generator`, `relay`, `sink`) — voir ADR-006 |
+| `particleGeneration` | number | 1 (si `nodeRole=generator`), sinon ignoré | Particules générées par seconde (taux d'émission) |
+| `maxParticleProcessing` | number | 60 | Particules maximum traitées par seconde |
+| `queue_size` | integer | ∞ | Taille maximale de la file d'attente du nœud (cf. `dropPolicy`) |
+| `processing_time` | number | 0 | Temps de traitement minimal par particule (ms) |
+| `failure_rate` | number | 0 | Probabilité [0–1] qu'une particule sortante soit droppée |
+| `dropPolicy` | enum | `tail` | Politique de drop quand la file est pleine : `tail` (drop l'entrante), `head` (drop la plus ancienne), `reject` (rejette l'entrante sans la stocker, équivalent à `tail` côté visuel) |
+
+#### Rôles `nodeRole`
+
+- **`generator`** — émet activement des particules selon `particleGeneration`.
+ Peut aussi recevoir et router du trafic entrant (le rôle ne désactive pas la
+ retransmission).
+- **`relay`** _(défaut)_ — reçoit, met en file, traite, route vers les liens
+ sortants. N'émet pas spontanément.
+- **`sink`** — absorbe les particules à l'arrivée, ne route pas. Utile pour
+ modéliser une destination terminale (logs, base de données, sortie système).
+
+> ⚠️ **V1 stricte** (ADR-006) : un graphe sans aucun nœud `nodeRole=generator`
+> n'animera pas de particules. Il faut annoter explicitement les sources.
### 1.3 Exemple Complet Nœud
@@ -66,8 +89,13 @@ ServerNode [
color="#4a90e2",
geometry="Box",
dimensions="{width: 2, height: 1, depth: 1}",
- particleGeneration=120,
- maxParticleProcessing=100
+ nodeRole="generator",
+ particleGeneration=2,
+ maxParticleProcessing=100,
+ queue_size=50,
+ dropPolicy="tail",
+ processing_time=10,
+ failure_rate=0.0
];
```
@@ -78,7 +106,7 @@ ServerNode [
### 2.1 Formule de Calcul
```javascript
-taille_finale = defaultNodeSize * (1 + 0.1 * sqrt(nombre_connexions_entrantes))
+taille_finale = defaultNodeSize * (1 + 0.1 * sqrt(nombre_connexions_entrantes));
```
### 2.2 Configuration Globale
@@ -87,7 +115,7 @@ taille_finale = defaultNodeSize * (1 + 0.1 * sqrt(nombre_connexions_entrantes))
digraph SystemGraph {
// Taille de base pour tous les nœuds
defaultNodeSize = 1.5;
-
+
// Activation du redimensionnement automatique
autoResize = true;
}
@@ -101,10 +129,10 @@ digraph SystemGraph {
```javascript
// À chaque cycle de simulation
-accumulation += (particules_generees + particules_reçues) - particules_traitees
+accumulation += particules_generees + particules_reçues - particules_traitees;
// Effet bloom proportionnel
-bloom_intensity = min(1.0, accumulation / 100)
+bloom_intensity = min(1.0, accumulation / 100);
```
### 3.2 États des Particules
@@ -120,13 +148,13 @@ bloom_intensity = min(1.0, accumulation / 100)
### 4.1 Spécification des Attributs
-| Attribut | Type | Défaut | Description |
-|----------|------|--------|-------------|
-| `label` | string | "" | Texte affiché sur le lien |
-| `color` | color | auto | Couleur du lien |
-| `maxParticleFlow` | number | 30 | Particules maximum transmises par minute |
-| `particleSpeed` | number | 1.0 | Vitesse de propagation (multiplier) |
-| `style` | enum | "solid" | Style visuel du lien |
+| Attribut | Type | Défaut | Description |
+| ----------------- | ------ | ------- | ---------------------------------------- |
+| `label` | string | "" | Texte affiché sur le lien |
+| `color` | color | auto | Couleur du lien |
+| `maxParticleFlow` | number | 30 | Particules maximum transmises par minute |
+| `particleSpeed` | number | 1.0 | Vitesse de propagation (multiplier) |
+| `style` | enum | "solid" | Style visuel du lien |
### 4.2 Styles de Liens
@@ -152,13 +180,13 @@ A -> B [
### 5.1 Paramètres Disponibles
-| Paramètre | Type | Défaut | Description |
-|-----------|------|--------|-------------|
-| `defaultNodeSize` | number | 1.0 | Taille de base des nœuds |
-| `particlesEnabled` | boolean | true | Activation système particules |
-| `autoResize` | boolean | false | Redimensionnement automatique |
-| `bloomEffect` | boolean | true | Effet bloom sur accumulation |
-| `autoColors` | boolean | true | Attribution automatique couleurs |
+| Paramètre | Type | Défaut | Description |
+| ------------------ | ------- | ------ | -------------------------------- |
+| `defaultNodeSize` | number | 1.0 | Taille de base des nœuds |
+| `particlesEnabled` | boolean | true | Activation système particules |
+| `autoResize` | boolean | false | Redimensionnement automatique |
+| `bloomEffect` | boolean | true | Effet bloom sur accumulation |
+| `autoColors` | boolean | true | Attribution automatique couleurs |
### 5.2 Exemple Configuration
@@ -170,7 +198,7 @@ digraph ParticleSystem {
autoResize = true;
bloomEffect = true;
autoColors = false; // Couleurs manuelles uniquement
-
+
// Reste du graphique...
}
```
@@ -184,7 +212,7 @@ digraph ParticleSystem {
Le panneau de contrôle 3D doit permettre la modification dynamique de :
- ✅ Activation/désactivation système particules
-- ✅ Activation/désactivation redimensionnement automatique
+- ✅ Activation/désactivation redimensionnement automatique
- ✅ Activation/désactivation effet bloom
- ✅ Activation/désactivation couleurs automatiques
- ✅ Ajustement `defaultNodeSize` global
@@ -193,6 +221,7 @@ Le panneau de contrôle 3D doit permettre la modification dynamique de :
### 6.2 Statistiques Temps Réel
Affichage en direct de :
+
- Nombre total de particules actives
- Taux de génération global (particules/minute)
- Taux de traitement global (particules/minute)
@@ -209,10 +238,18 @@ Affichage en direct de :
- `maxParticleFlow > 0`
- `particleSpeed > 0`
- `defaultNodeSize > 0`
+- `nodeRole ∈ {generator, relay, sink}`
+- `queue_size > 0` (entier strict)
+- `processing_time >= 0`
+- `failure_rate ∈ [0, 1]`
+- `dropPolicy ∈ {tail, head, reject}` — n'a de sens que si `queue_size` est défini
+- `dropPolicy` défini sans `queue_size` → **avertissement** (sera ignoré : file illimitée)
+- `particleGeneration > 0` sur un `nodeRole=relay|sink` → **avertissement** (sera ignoré)
### 7.2 Validation Géométries
Chaque géométrie doit avoir ses dimensions appropriées :
+
- **Sphere** : `radius > 0`
- **Box** : `width, height, depth > 0`
- **Cylinder** : `radius, height > 0`
@@ -253,6 +290,7 @@ Messages explicites pour faciliter le debugging :
### 9.1 Nouvelles Géométries
Architecture permettant l'ajout de :
+
- Géométries personnalisées via plugins
- Géométries procedurales (fractales, etc.)
- Import de modèles 3D (.obj, .gltf)
@@ -260,6 +298,7 @@ Architecture permettant l'ajout de :
### 9.2 Nouveaux Attributs
Extension prévue pour :
+
- Animations automatiques (rotation, pulsation)
- Physique avancée (collision, gravité)
- Effets visuels (particules custom, shaders)
diff --git a/doc/dot-3d/user-guide.md b/doc/dot-3d/user-guide.md
index 2eb6ed9..b29d115 100644
--- a/doc/dot-3d/user-guide.md
+++ b/doc/dot-3d/user-guide.md
@@ -15,17 +15,18 @@ digraph FirstExample {
// Configuration de base
defaultNodeSize = 1.0;
particlesEnabled = true;
-
+
// Nœuds avec géométries 3D
A [label="Serveur", geometry="Box", particleGeneration=30];
B [label="Client", geometry="Sphere", maxParticleProcessing=60];
-
+
// Lien avec flux de particules
A -> B [maxParticleFlow=20, particleSpeed=1.2];
}
```
### Résultat
+
- **Serveur A** : Cube générant 30 particules/minute
- **Client B** : Sphère traitant jusqu'à 60 particules/minute
- **Flux** : 20 particules max/minute à vitesse 1.2x
@@ -35,15 +36,17 @@ digraph FirstExample {
## 📐 Géométries 3D Disponibles
### 1. Sphere (Sphère)
+
```dot
NodeSphere [
- geometry="Sphere",
+ geometry="Sphere",
dimensions="{radius: 1.5}",
color="#4CAF50"
];
```
### 2. Box (Boîte/Cube)
+
```dot
NodeBox [
geometry="Box",
@@ -53,6 +56,7 @@ NodeBox [
```
### 3. Cylinder (Cylindre)
+
```dot
NodeCylinder [
geometry="Cylinder",
@@ -62,6 +66,7 @@ NodeCylinder [
```
### 4. Cone (Cône)
+
```dot
NodeCone [
geometry="Cone",
@@ -71,6 +76,7 @@ NodeCone [
```
### 5. Torus (Tore)
+
```dot
NodeTorus [
geometry="Torus",
@@ -83,41 +89,84 @@ NodeTorus [
## ⚡ Système de Particules
+> 🆕 **Depuis ADR-006**, la simulation est événementielle (DES) : seuls les
+> nœuds dont le rôle est explicitement `generator` émettent des particules.
+> Les nœuds sans `nodeRole` sont considérés comme `relay` (ils transmettent
+> sans émettre). Un graphe sans aucun `generator` reste statique.
+
+### Rôles des nœuds (`nodeRole`)
+
+| Valeur | Comportement | Cas d'usage typique |
+| ------------------ | ------------------------------------------------------------------------------------- | --------------------------------- |
+| `generator` | Émet des particules à `particleGeneration` p/s. Peut aussi router son trafic entrant. | Source, capteur, client émetteur |
+| `relay` _(défaut)_ | Reçoit, met en file, traite, route vers les liens sortants. | Routeur, processeur intermédiaire |
+| `sink` | Absorbe les particules à l'arrivée, ne route rien. | Destination finale, log, drain |
+
### Configuration des Nœuds
#### Nœud Générateur
+
```dot
Generator [
label="Générateur",
- particleGeneration=120, // 120 particules/minute
- maxParticleProcessing=60, // Traite 60 max/minute
+ nodeRole="generator",
+ particleGeneration=2, // 2 particules/seconde
+ maxParticleProcessing=60, // Traite 60 max/seconde (s'il reçoit aussi)
geometry="Cone",
color="#ff4444"
];
```
-#### Nœud Processeur
+#### Nœud Processeur (relay)
+
```dot
Processor [
label="Processeur",
- particleGeneration=0, // Ne génère pas
- maxParticleProcessing=200, // Traite 200 max/minute
+ // nodeRole non précisé → relay par défaut
+ maxParticleProcessing=200, // Traite 200 max/seconde
+ queue_size=100, // File limitée à 100
+ dropPolicy="tail", // Drop l'entrante quand pleine
+ processing_time=5, // 5 ms par particule
geometry="Box",
color="#44ff44"
];
```
-#### Nœud Accumulateur
+#### Nœud Tampon avec drop visible
+
```dot
Buffer [
- label="Tampon",
- particleGeneration=30, // Génération modérée
+ label="Tampon saturable",
+ nodeRole="relay",
maxParticleProcessing=15, // Traite lentement
+ queue_size=20, // File petite
+ dropPolicy="head", // Drop la plus ancienne
+ failure_rate=0.02, // 2 % d'échec à la sortie
geometry="Cylinder",
color="#4444ff"
];
```
+#### Nœud Puits
+
+```dot
+Sink [
+ label="Destination",
+ nodeRole="sink",
+ geometry="Sphere",
+ color="#888888"
+];
+```
+
+### Cumul et drop : ce qui se passe à l'écran
+
+- **Accumulation** : la taille visuelle d'un nœud croît avec son nombre de
+ particules en file (jusqu'à 2× sa taille de base à saturation).
+- **Halo de saturation** : orange si file > 80 % de `queue_size`, rouge si pleine.
+- **Drop** : flash rouge bref + compteur incrémenté sur le nœud.
+- **Stats HUD** : `Drops` ajouté à côté de `Particules / Latence / Goulots`,
+ alimenté en temps réel par le simulator.
+
### Configuration des Liens
```dot
@@ -145,6 +194,7 @@ Processor -> Buffer [
### Couleurs Supportées
#### Format Hexadécimal
+
```dot
A [color="#ff0000"]; // Rouge
B [color="#00ff00"]; // Vert
@@ -152,6 +202,7 @@ C [color="#0000ff"]; // Bleu
```
#### Format RGB
+
```dot
A [color="rgb(255, 0, 0)"]; // Rouge
B [color="rgb(0, 255, 0)"]; // Vert
@@ -159,6 +210,7 @@ C [color="rgb(0, 0, 255)"]; // Bleu
```
#### Couleurs Nommées
+
```dot
A [color="red"];
B [color="green"];
@@ -190,19 +242,19 @@ A -> D [style="dotted"];
digraph NetworkSimulation {
// Taille de base des nœuds
defaultNodeSize = 1.5;
-
+
// Activation système particules
particlesEnabled = true;
-
+
// Redimensionnement automatique selon connexions
autoResize = true;
-
+
// Effet bloom sur accumulation
bloomEffect = true;
-
+
// Attribution couleurs automatique
autoColors = false; // Désactivé pour couleurs manuelles
-
+
// Vos nœuds et liens...
}
```
@@ -216,6 +268,7 @@ taille_finale = defaultNodeSize × (1 + 0.1 × √(connexions_entrantes))
```
**Exemples :**
+
- 0 connexion : taille = 1.0 × (1 + 0) = **1.0**
- 4 connexions : taille = 1.0 × (1 + 0.2) = **1.2**
- 16 connexions : taille = 1.0 × (1 + 0.4) = **1.4**
@@ -227,6 +280,7 @@ taille_finale = defaultNodeSize × (1 + 0.1 × √(connexions_entrantes))
### États des Particules
#### Accumulation Normale
+
```dot
NormalNode [
particleGeneration=60,
@@ -235,6 +289,7 @@ NormalNode [
```
#### Accumulation Critique
+
```dot
BottleneckNode [
particleGeneration=100,
@@ -245,6 +300,7 @@ BottleneckNode [
### Effet Bloom
Quand l'accumulation dépasse 80 particules, le nœud commence à "briller" :
+
- **Accumulation 0-80** : Apparence normale
- **Accumulation 80-100** : Effet bloom progressif
- **Accumulation >100** : Bloom maximum (intensité 1.0)
@@ -253,6 +309,70 @@ Quand l'accumulation dépasse 80 particules, le nœud commence à "briller" :
## 💡 Exemples Pratiques Complets
+### Exemple 0 : Pipeline DES avec goulot et drops (ADR-006)
+
+Le plus pédagogique pour découvrir la simulation événementielle. Une source rapide
+envoie 10 particules/seconde dans un goulot qui n'en traite que 5/s avec une file
+de 5 emplacements et un `dropPolicy="tail"` — donc, dès que la file est saturée,
+les nouvelles particules sont droppées avant d'entrer.
+
+```dot
+digraph SaturationDemo {
+ defaultNodeSize = 1.2;
+ particlesEnabled = true;
+
+ FastSource [
+ label="Source rapide",
+ nodeRole="generator",
+ particleGeneration=10, // 10 particules/seconde
+ geometry="Cone"
+ ];
+
+ Bottleneck [
+ label="Goulot",
+ nodeRole="relay",
+ maxParticleProcessing=1, // 1 slot de traitement parallèle
+ processing_time=200, // 200 ms par particule → débit 5/s
+ queue_size=5, // file de 5 emplacements
+ dropPolicy="tail", // drop l'entrante quand la file est pleine
+ geometry="Cylinder"
+ ];
+
+ Sink [
+ label="Sortie",
+ nodeRole="sink",
+ geometry="Sphere"
+ ];
+
+ FastSource -> Bottleneck [particleSpeed=6];
+ Bottleneck -> Sink [particleSpeed=6];
+}
+```
+
+**Ce qui se passe à l'écran après ▶** :
+
+1. **Émission régulière** : la source émet 1 particule toutes les 100 ms.
+2. **Accumulation** : le goulot grossit visuellement à mesure que sa file se remplit
+ (jusqu'à 2× sa taille de base).
+3. **Halo** : à 80 % de remplissage, le nœud devient orange. À 100 %, il devient rouge.
+4. **Flash drop** : à chaque drop (queue pleine), le nœud flashe rouge vif pendant 200 ms.
+5. **HUD** : les chips `Drops`, `File max` et `Débit` (en bas à droite) chiffrent
+ l'effet en temps réel. Survole-les pour avoir l'explication.
+
+Change `dropPolicy="tail"` en `"head"` pour voir la différence : c'est alors la
+particule la plus ancienne qui est jetée à chaque saturation. La file reste à
+la même taille, mais le débit observé en sortie change subtilement parce que
+les particules les plus récentes ont systématiquement la priorité.
+
+**Variantes à expérimenter** :
+
+- `maxParticleProcessing=5` → 5 slots parallèles, débit 25 p/s, file vide en
+ permanence si l'entrée reste à 10/s.
+- `failure_rate=0.1` → 10 % des particules sortantes sont droppées (sémantique
+ "le service échoue").
+- Ajoute un 2e générateur convergent pour reproduire le scénario `convergence`
+ des tests d'intégration.
+
### Exemple 1 : Réseau de Distribution
```dot
@@ -262,7 +382,7 @@ digraph DistributionNetwork {
particlesEnabled = true;
autoResize = true;
bloomEffect = true;
-
+
// Serveur principal
MainServer [
label="Serveur Principal",
@@ -272,17 +392,17 @@ digraph DistributionNetwork {
maxParticleProcessing=180,
color="#2196F3"
];
-
+
// Serveurs régionaux
RegionA [
label="Région A",
- geometry="Cylinder",
+ geometry="Cylinder",
dimensions="{radius: 0.8, height: 1.5}",
particleGeneration=50,
maxParticleProcessing=120,
color="#4CAF50"
];
-
+
RegionB [
label="Région B",
geometry="Cylinder",
@@ -291,7 +411,7 @@ digraph DistributionNetwork {
maxParticleProcessing=100,
color="#4CAF50"
];
-
+
// Clients finaux
ClientGroup [
label="Clients",
@@ -301,7 +421,7 @@ digraph DistributionNetwork {
maxParticleProcessing=200,
color="#FF9800"
];
-
+
// Flux de données
MainServer -> RegionA [
label="Flux A",
@@ -309,20 +429,20 @@ digraph DistributionNetwork {
particleSpeed=1.5,
color="#1976D2"
];
-
+
MainServer -> RegionB [
- label="Flux B",
+ label="Flux B",
maxParticleFlow=60,
particleSpeed=1.5,
color="#1976D2"
];
-
+
RegionA -> ClientGroup [
maxParticleFlow=100,
particleSpeed=1.0,
color="#388E3C"
];
-
+
RegionB -> ClientGroup [
maxParticleFlow=80,
particleSpeed=1.0,
@@ -341,7 +461,7 @@ digraph ProcessingPipeline {
autoResize = false; // Tailles fixes
bloomEffect = true;
autoColors = false;
-
+
// Étapes du pipeline
Input [
label="Entrée",
@@ -351,7 +471,7 @@ digraph ProcessingPipeline {
maxParticleProcessing=150,
color="#E91E63"
];
-
+
Filter [
label="Filtrage",
geometry="Box",
@@ -360,7 +480,7 @@ digraph ProcessingPipeline {
maxParticleProcessing=100, // Goulot d'étranglement
color="#FF5722"
];
-
+
Transform [
label="Transformation",
geometry="Cylinder",
@@ -369,7 +489,7 @@ digraph ProcessingPipeline {
maxParticleProcessing=120,
color="#FF9800"
];
-
+
Output [
label="Sortie",
geometry="Sphere",
@@ -378,7 +498,7 @@ digraph ProcessingPipeline {
maxParticleProcessing=200,
color="#4CAF50"
];
-
+
// Pipeline avec vitesses variées
Input -> Filter [
maxParticleFlow=120,
@@ -386,14 +506,14 @@ digraph ProcessingPipeline {
style="solid",
color="#D32F2F"
];
-
+
Filter -> Transform [
maxParticleFlow=90, // Réduit par le filtre
particleSpeed=1.0, // Normal
style="dashed",
color="#F57C00"
];
-
+
Transform -> Output [
maxParticleFlow=100,
particleSpeed=1.5, // Accéléré
@@ -410,28 +530,32 @@ digraph ProcessingPipeline {
### Messages d'Erreur Courants
#### Erreur de Géométrie
+
```
❌ Erreur ligne 12: geometry "InvalidShape" non supportée
Géométries supportées: Sphere, Box, Cylinder, Cone, Torus
```
#### Erreur de Dimensions
+
```
❌ Erreur ligne 15: dimensions manquantes pour geometry="Box"
Box requiert: width, height, depth
```
#### Erreur de Contraintes
+
```
❌ Erreur ligne 8: particleGeneration ne peut pas être négatif
Valeur reçue: -10, minimum requis: 0
```
### Validation Réussie
+
```
✅ Graphique validé avec succès
• 8 nœuds détectés
- • 12 liens configurés
+ • 12 liens configurés
• Système particules: activé
• Géométries: 3 Box, 2 Sphere, 2 Cylinder, 1 Cone
```
@@ -441,22 +565,26 @@ digraph ProcessingPipeline {
## 🎮 Utilisation dans VortexFlow
### 1. Édition
+
- Ouvrez l'**Éditeur de Graphiques**
- Sélectionnez le mode **"DOT 3D"**
- Saisissez votre code DOT étendu
- La validation se fait en temps réel
### 2. Prévisualisation
+
- Cliquez sur **"Aperçu 3D"**
- Le graphique s'affiche avec géométries et particules
- Le **panneau de contrôle** permet d'ajuster les paramètres
### 3. Simulation
+
- Lancez la simulation avec **"Démarrer"**
- Observez les particules circuler en temps réel
- Consultez les métriques dans le panneau
### 4. Optimisation
+
- Utilisez les contrôles pour ajuster :
- Taille des nœuds
- Espacement
diff --git a/doc/dot-3d/validation-rules.md b/doc/dot-3d/validation-rules.md
index aa28b6d..06f855c 100644
--- a/doc/dot-3d/validation-rules.md
+++ b/doc/dot-3d/validation-rules.md
@@ -14,13 +14,14 @@ Ce document définit toutes les règles de validation pour la grammaire DOT 3D
interface SyntaxValidation {
// Validation structure de base
validateGraphDeclaration(): ValidationResult;
- validateNodeDeclaration(): ValidationResult;
+ validateNodeDeclaration(): ValidationResult;
validateEdgeDeclaration(): ValidationResult;
validateAttributeList(): ValidationResult;
}
```
#### Règles de Base
+
- ✅ Graphique doit commencer par `digraph` ou `graph`
- ✅ Accolades `{` `}` correctement appariées
- ✅ Identifiants valides (lettres, chiffres, underscore)
@@ -46,11 +47,11 @@ interface ExtendedSyntaxValidation {
```typescript
enum GeometryType {
- SPHERE = "Sphere",
- BOX = "Box",
- CYLINDER = "Cylinder",
- CONE = "Cone",
- TORUS = "Torus"
+ SPHERE = 'Sphere',
+ BOX = 'Box',
+ CYLINDER = 'Cylinder',
+ CONE = 'Cone',
+ TORUS = 'Torus',
}
interface GeometryValidation {
@@ -63,80 +64,85 @@ interface GeometryValidation {
### 2.2 Contraintes par Géométrie
#### Sphere
+
```typescript
const SphereValidation: GeometryValidation = {
- geometry: "Sphere",
- requiredDimensions: ["radius"],
+ geometry: 'Sphere',
+ requiredDimensions: ['radius'],
validateDimensions: (dims) => {
if (!dims.radius) return error("Sphere requires 'radius' dimension");
- if (dims.radius <= 0) return error("Sphere radius must be > 0");
+ if (dims.radius <= 0) return error('Sphere radius must be > 0');
return success();
- }
+ },
};
```
#### Box
+
```typescript
const BoxValidation: GeometryValidation = {
- geometry: "Box",
- requiredDimensions: ["width", "height", "depth"],
+ geometry: 'Box',
+ requiredDimensions: ['width', 'height', 'depth'],
validateDimensions: (dims) => {
- const required = ["width", "height", "depth"];
+ const required = ['width', 'height', 'depth'];
for (const dim of required) {
if (!dims[dim]) return error(`Box requires '${dim}' dimension`);
if (dims[dim] <= 0) return error(`Box ${dim} must be > 0`);
}
return success();
- }
+ },
};
```
#### Cylinder
+
```typescript
const CylinderValidation: GeometryValidation = {
- geometry: "Cylinder",
- requiredDimensions: ["radius", "height"],
+ geometry: 'Cylinder',
+ requiredDimensions: ['radius', 'height'],
validateDimensions: (dims) => {
if (!dims.radius) return error("Cylinder requires 'radius' dimension");
if (!dims.height) return error("Cylinder requires 'height' dimension");
- if (dims.radius <= 0) return error("Cylinder radius must be > 0");
- if (dims.height <= 0) return error("Cylinder height must be > 0");
+ if (dims.radius <= 0) return error('Cylinder radius must be > 0');
+ if (dims.height <= 0) return error('Cylinder height must be > 0');
return success();
- }
+ },
};
```
#### Cone
+
```typescript
const ConeValidation: GeometryValidation = {
- geometry: "Cone",
- requiredDimensions: ["radius", "height"],
+ geometry: 'Cone',
+ requiredDimensions: ['radius', 'height'],
validateDimensions: (dims) => {
if (!dims.radius) return error("Cone requires 'radius' dimension");
if (!dims.height) return error("Cone requires 'height' dimension");
- if (dims.radius <= 0) return error("Cone radius must be > 0");
- if (dims.height <= 0) return error("Cone height must be > 0");
+ if (dims.radius <= 0) return error('Cone radius must be > 0');
+ if (dims.height <= 0) return error('Cone height must be > 0');
return success();
- }
+ },
};
```
#### Torus
+
```typescript
const TorusValidation: GeometryValidation = {
- geometry: "Torus",
- requiredDimensions: ["tube", "tubularSegments", "radialSegments"],
+ geometry: 'Torus',
+ requiredDimensions: ['tube', 'tubularSegments', 'radialSegments'],
validateDimensions: (dims) => {
if (!dims.tube) return error("Torus requires 'tube' dimension");
if (!dims.tubularSegments) return error("Torus requires 'tubularSegments'");
if (!dims.radialSegments) return error("Torus requires 'radialSegments'");
-
- if (dims.tube <= 0) return error("Torus tube must be > 0");
- if (dims.tubularSegments < 3) return error("Torus tubularSegments must be >= 3");
- if (dims.radialSegments < 3) return error("Torus radialSegments must be >= 3");
-
+
+ if (dims.tube <= 0) return error('Torus tube must be > 0');
+ if (dims.tubularSegments < 3) return error('Torus tubularSegments must be >= 3');
+ if (dims.radialSegments < 3) return error('Torus radialSegments must be >= 3');
+
return success();
- }
+ },
};
```
@@ -148,22 +154,91 @@ const TorusValidation: GeometryValidation = {
```typescript
interface ParticleNodeValidation {
+ validateNodeRole(value: string): ValidationResult;
validateParticleGeneration(value: number): ValidationResult;
validateMaxParticleProcessing(value: number): ValidationResult;
+ validateQueueSize(value: number): ValidationResult;
+ validateProcessingTime(value: number): ValidationResult;
+ validateFailureRate(value: number): ValidationResult;
+ validateDropPolicy(value: string): ValidationResult;
}
+const NODE_ROLES = ['generator', 'relay', 'sink'] as const;
+const DROP_POLICIES = ['tail', 'head', 'reject'] as const;
+
const ParticleNodeRules = {
+ validateNodeRole: (value: string) => {
+ if (!NODE_ROLES.includes(value as any)) {
+ return error(`nodeRole must be one of: ${NODE_ROLES.join(', ')}`);
+ }
+ return success();
+ },
+
validateParticleGeneration: (value: number) => {
- if (value < 0) return error("particleGeneration cannot be negative");
- if (value > 10000) return warning("High particle generation may impact performance");
+ if (value < 0) return error('particleGeneration cannot be negative');
+ if (value > 10000) return warning('High particle generation may impact performance');
return success();
},
-
+
validateMaxParticleProcessing: (value: number) => {
- if (value <= 0) return error("maxParticleProcessing must be > 0");
- if (value > 5000) return warning("High processing rate may impact performance");
+ if (value <= 0) return error('maxParticleProcessing must be > 0');
+ if (value > 5000) return warning('High processing rate may impact performance');
return success();
- }
+ },
+
+ validateQueueSize: (value: number) => {
+ if (!Number.isInteger(value)) return error('queue_size must be an integer');
+ if (value <= 0) return error('queue_size must be > 0');
+ if (value > 10000) return warning('Very large queue_size may impact memory');
+ return success();
+ },
+
+ validateProcessingTime: (value: number) => {
+ if (value < 0) return error('processing_time cannot be negative');
+ return success();
+ },
+
+ validateFailureRate: (value: number) => {
+ if (value < 0 || value > 1) return error('failure_rate must be in [0, 1]');
+ return success();
+ },
+
+ validateDropPolicy: (value: string) => {
+ if (!DROP_POLICIES.includes(value as any)) {
+ return error(`dropPolicy must be one of: ${DROP_POLICIES.join(', ')}`);
+ }
+ return success();
+ },
+};
+```
+
+### 3.1.bis Validation de cohérence inter-attributs
+
+```typescript
+// Warnings, not errors — the validator still accepts the graph
+const NodeCoherenceRules = {
+ // dropPolicy without queue_size is meaningless (queue is unbounded)
+ validateDropPolicyRequiresQueueSize: (node: Node3D) => {
+ if (node.dropPolicy !== undefined && node.queue_size === undefined) {
+ return warning(
+ `Node ${node.id}: dropPolicy="${node.dropPolicy}" has no effect ` +
+ `without queue_size — the queue is unbounded and never drops.`
+ );
+ }
+ return success();
+ },
+
+ // particleGeneration on a non-generator role is ignored at runtime
+ validateGenerationRequiresGeneratorRole: (node: Node3D) => {
+ const role = node.nodeRole ?? 'relay';
+ if (node.particleGeneration && node.particleGeneration > 0 && role !== 'generator') {
+ return warning(
+ `Node ${node.id}: particleGeneration=${node.particleGeneration} ` +
+ `is ignored because nodeRole="${role}" (only "generator" emits).`
+ );
+ }
+ return success();
+ },
};
```
@@ -177,16 +252,16 @@ interface ParticleEdgeValidation {
const ParticleEdgeRules = {
validateMaxParticleFlow: (value: number) => {
- if (value <= 0) return error("maxParticleFlow must be > 0");
- if (value > 1000) return warning("High particle flow may cause congestion");
+ if (value <= 0) return error('maxParticleFlow must be > 0');
+ if (value > 1000) return warning('High particle flow may cause congestion');
return success();
},
-
+
validateParticleSpeed: (value: number) => {
- if (value <= 0) return error("particleSpeed must be > 0");
- if (value > 10) return warning("Very high particle speed may reduce visibility");
+ if (value <= 0) return error('particleSpeed must be > 0');
+ if (value > 10) return warning('Very high particle speed may reduce visibility');
return success();
- }
+ },
};
```
@@ -207,30 +282,30 @@ interface GlobalConfigValidation {
const GlobalConfigRules = {
validateDefaultNodeSize: (value: number) => {
- if (value <= 0) return error("defaultNodeSize must be > 0");
- if (value > 10) return warning("Very large node size may cause overlap");
+ if (value <= 0) return error('defaultNodeSize must be > 0');
+ if (value > 10) return warning('Very large node size may cause overlap');
return success();
},
-
+
validateParticlesEnabled: (value: boolean) => {
- if (typeof value !== 'boolean') return error("particlesEnabled must be true or false");
+ if (typeof value !== 'boolean') return error('particlesEnabled must be true or false');
return success();
},
-
+
validateAutoResize: (value: boolean) => {
- if (typeof value !== 'boolean') return error("autoResize must be true or false");
+ if (typeof value !== 'boolean') return error('autoResize must be true or false');
return success();
},
-
+
validateBloomEffect: (value: boolean) => {
- if (typeof value !== 'boolean') return error("bloomEffect must be true or false");
+ if (typeof value !== 'boolean') return error('bloomEffect must be true or false');
return success();
},
-
+
validateAutoColors: (value: boolean) => {
- if (typeof value !== 'boolean') return error("autoColors must be true or false");
+ if (typeof value !== 'boolean') return error('autoColors must be true or false');
return success();
- }
+ },
};
```
@@ -255,29 +330,39 @@ const ColorRules = {
}
return success();
},
-
+
validateRgbColor: (color: string) => {
const rgbPattern = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
const match = color.match(rgbPattern);
if (!match) return error(`Invalid RGB color: ${color}. Format: rgb(r,g,b)`);
-
+
const [, r, g, b] = match;
if (+r > 255 || +g > 255 || +b > 255) {
- return error("RGB values must be between 0 and 255");
+ return error('RGB values must be between 0 and 255');
}
return success();
},
-
+
validateNamedColor: (color: string) => {
const namedColors = [
- 'red', 'blue', 'green', 'yellow', 'orange', 'purple',
- 'pink', 'cyan', 'magenta', 'black', 'white', 'gray'
+ 'red',
+ 'blue',
+ 'green',
+ 'yellow',
+ 'orange',
+ 'purple',
+ 'pink',
+ 'cyan',
+ 'magenta',
+ 'black',
+ 'white',
+ 'gray',
];
if (!namedColors.includes(color)) {
return error(`Unknown color name: ${color}. Supported: ${namedColors.join(', ')}`);
}
return success();
- }
+ },
};
```
@@ -295,40 +380,39 @@ interface FlowConsistencyValidation {
const FlowConsistencyRules = {
validateNodeCapacity: (node: Node3D) => {
- const incomingFlow = node.incomingEdges.reduce((sum, edge) =>
- sum + edge.maxParticleFlow, 0);
+ const incomingFlow = node.incomingEdges.reduce((sum, edge) => sum + edge.maxParticleFlow, 0);
const nodeGeneration = node.particleGeneration;
const totalInput = incomingFlow + nodeGeneration;
-
+
if (totalInput > node.maxParticleProcessing * 1.5) {
return warning(
`Node ${node.id}: Total input (${totalInput}) significantly exceeds ` +
- `processing capacity (${node.maxParticleProcessing}). ` +
- `This will cause significant accumulation.`
+ `processing capacity (${node.maxParticleProcessing}). ` +
+ `This will cause significant accumulation.`
);
}
-
+
return success();
},
-
+
validateNetworkFlow: (graph: Graph3D) => {
let totalGeneration = 0;
let totalProcessing = 0;
-
+
for (const node of graph.nodes) {
totalGeneration += node.particleGeneration;
totalProcessing += node.maxParticleProcessing;
}
-
+
if (totalGeneration > totalProcessing * 1.2) {
return warning(
`Network generates ${totalGeneration} particles/min but can only process ` +
- `${totalProcessing}/min. Consider increasing processing capacity.`
+ `${totalProcessing}/min. Consider increasing processing capacity.`
);
}
-
+
return success();
- }
+ },
};
```
@@ -342,39 +426,37 @@ interface PerformanceValidation {
const PerformanceRules = {
validateParticleCount: (graph: Graph3D) => {
- const totalParticles = graph.nodes.reduce((sum, node) =>
- sum + node.particleGeneration, 0);
-
+ const totalParticles = graph.nodes.reduce((sum, node) => sum + node.particleGeneration, 0);
+
if (totalParticles > 1000) {
return warning(
`High particle generation rate (${totalParticles}/min). ` +
- `Consider reducing for better performance.`
+ `Consider reducing for better performance.`
);
}
-
+
if (totalParticles > 5000) {
return error(
`Extremely high particle rate (${totalParticles}/min). ` +
- `This may cause severe performance issues.`
+ `This may cause severe performance issues.`
);
}
-
+
return success();
},
-
+
validateGeometryComplexity: (graph: Graph3D) => {
- const complexGeometries = graph.nodes.filter(node =>
- node.geometry === 'Torus').length;
-
+ const complexGeometries = graph.nodes.filter((node) => node.geometry === 'Torus').length;
+
if (complexGeometries > 20) {
return warning(
`Many complex geometries (${complexGeometries} Torus). ` +
- `Consider using simpler shapes for better performance.`
+ `Consider using simpler shapes for better performance.`
);
}
-
+
return success();
- }
+ },
};
```
@@ -386,10 +468,10 @@ const PerformanceRules = {
```typescript
enum ErrorType {
- SYNTAX_ERROR = "SYNTAX_ERROR",
- SEMANTIC_ERROR = "SEMANTIC_ERROR",
- WARNING = "WARNING",
- INFO = "INFO"
+ SYNTAX_ERROR = 'SYNTAX_ERROR',
+ SEMANTIC_ERROR = 'SEMANTIC_ERROR',
+ WARNING = 'WARNING',
+ INFO = 'INFO',
}
interface ValidationMessage {
@@ -405,34 +487,34 @@ const ErrorTemplates = {
INVALID_GEOMETRY: (line: number, geometry: string) => ({
type: ErrorType.SEMANTIC_ERROR,
line,
- code: "INVALID_GEOMETRY",
+ code: 'INVALID_GEOMETRY',
message: `Geometry "${geometry}" is not supported`,
- suggestion: `Supported geometries: Sphere, Box, Cylinder, Cone, Torus`
+ suggestion: `Supported geometries: Sphere, Box, Cylinder, Cone, Torus`,
}),
-
+
MISSING_DIMENSIONS: (line: number, geometry: string, missing: string[]) => ({
type: ErrorType.SEMANTIC_ERROR,
line,
- code: "MISSING_DIMENSIONS",
+ code: 'MISSING_DIMENSIONS',
message: `Geometry "${geometry}" is missing required dimensions: ${missing.join(', ')}`,
- suggestion: `Add dimensions="{${missing.map(d => d + ': value').join(', ')}}"}`
+ suggestion: `Add dimensions="{${missing.map((d) => d + ': value').join(', ')}}"}`,
}),
-
+
NEGATIVE_VALUE: (line: number, attribute: string, value: number) => ({
type: ErrorType.SEMANTIC_ERROR,
line,
- code: "NEGATIVE_VALUE",
+ code: 'NEGATIVE_VALUE',
message: `Attribute "${attribute}" cannot be negative (got: ${value})`,
- suggestion: `Use a positive value for ${attribute}`
+ suggestion: `Use a positive value for ${attribute}`,
}),
-
+
PERFORMANCE_WARNING: (line: number, attribute: string, value: number, threshold: number) => ({
type: ErrorType.WARNING,
line,
- code: "PERFORMANCE_WARNING",
+ code: 'PERFORMANCE_WARNING',
message: `High ${attribute} value (${value}) may impact performance`,
- suggestion: `Consider reducing below ${threshold} for optimal performance`
- })
+ suggestion: `Consider reducing below ${threshold} for optimal performance`,
+ }),
};
```
@@ -441,24 +523,30 @@ const ErrorTemplates = {
```typescript
class ValidationReporter {
static formatMessage(msg: ValidationMessage): string {
- const prefix = msg.type === ErrorType.SYNTAX_ERROR || msg.type === ErrorType.SEMANTIC_ERROR
- ? "❌" : msg.type === ErrorType.WARNING ? "⚠️" : "ℹ️";
-
+ const prefix =
+ msg.type === ErrorType.SYNTAX_ERROR || msg.type === ErrorType.SEMANTIC_ERROR
+ ? '❌'
+ : msg.type === ErrorType.WARNING
+ ? '⚠️'
+ : 'ℹ️';
+
let formatted = `${prefix} ${msg.type}`;
if (msg.line) formatted += ` ligne ${msg.line}`;
formatted += `: ${msg.message}`;
-
+
if (msg.suggestion) {
formatted += `\n 💡 Suggestion: ${msg.suggestion}`;
}
-
+
return formatted;
}
-
+
static summarizeValidation(messages: ValidationMessage[]): string {
- const errors = messages.filter(m => m.type === ErrorType.SEMANTIC_ERROR || m.type === ErrorType.SYNTAX_ERROR);
- const warnings = messages.filter(m => m.type === ErrorType.WARNING);
-
+ const errors = messages.filter(
+ (m) => m.type === ErrorType.SEMANTIC_ERROR || m.type === ErrorType.SYNTAX_ERROR
+ );
+ const warnings = messages.filter((m) => m.type === ErrorType.WARNING);
+
if (errors.length === 0) {
return `✅ Validation réussie (${warnings.length} avertissement${warnings.length !== 1 ? 's' : ''})`;
} else {
@@ -478,35 +566,36 @@ class ValidationReporter {
class DOT3DValidator {
validate(dotContent: string): ValidationResult {
const results: ValidationMessage[] = [];
-
+
// 1. Validation syntaxique
results.push(...this.validateSyntax(dotContent));
- if (results.some(r => r.type === ErrorType.SYNTAX_ERROR)) {
+ if (results.some((r) => r.type === ErrorType.SYNTAX_ERROR)) {
return { success: false, messages: results };
}
-
+
// 2. Parse en AST
const ast = this.parse(dotContent);
-
+
// 3. Validation sémantique
results.push(...this.validateGeometry(ast));
results.push(...this.validateParticles(ast));
results.push(...this.validateColors(ast));
results.push(...this.validateGlobalConfig(ast));
-
+
// 4. Validation de cohérence
results.push(...this.validateConsistency(ast));
-
+
// 5. Validation performance
results.push(...this.validatePerformance(ast));
-
- const hasErrors = results.some(r =>
- r.type === ErrorType.SYNTAX_ERROR || r.type === ErrorType.SEMANTIC_ERROR);
-
+
+ const hasErrors = results.some(
+ (r) => r.type === ErrorType.SYNTAX_ERROR || r.type === ErrorType.SEMANTIC_ERROR
+ );
+
return {
success: !hasErrors,
messages: results,
- summary: ValidationReporter.summarizeValidation(results)
+ summary: ValidationReporter.summarizeValidation(results),
};
}
}
diff --git a/frontend/doc/RENDERER.md b/frontend/doc/RENDERER.md
index 750aab9..a61155b 100644
--- a/frontend/doc/RENDERER.md
+++ b/frontend/doc/RENDERER.md
@@ -55,6 +55,7 @@ and `simulationRunning` are true. An idle graph is static.
**Why**: the default of `3d-force-graph` is to keep particles flowing forever
once enabled, which:
+
- burns CPU/GPU when the user isn't watching a sim,
- visually misleads users who think a graph "is simulating" when it's just
a screensaver.
@@ -89,6 +90,7 @@ links. A visited set prevents cycle storms (depth cap 15).
`(1 / particleSpeed) * 16.67 ms` (one frame at 60 fps scaled by speed).
**Emitter definition**:
+
- If any node defines `particleGeneration > 0`, only those are emitters.
- Otherwise, every node emits one particle.
@@ -105,6 +107,7 @@ defeat that.
**What**: when no node defines `particleGeneration` / `maxParticleProcessing`,
the simulation effect falls back to:
+
- particle count = `scene.particles.length` (whatever 3d-force-graph happens
to be displaying)
- average traversal latency = derived from `particleSpeed`
@@ -138,7 +141,41 @@ write a parallel init path (e.g. for a "load template" feature).
---
-## 8. DOT parsing pipeline (with fallback)
+## 8.bis DES visual hooks (Phase 5)
+
+When the DES simulator owns the simulation (i.e. at least one
+`nodeRole=generator` node is declared — ADR-006), four visual behaviours
+are applied via accessor overrides:
+
+- **Queue growth on relays** — `nodeVal(node)` returns
+ `baseSize * (1 + min(1, pending/queue_size))`, capped at 2× when the
+ queue is full. Empty queue → normal size, full queue → twice as big.
+- **Saturation halo** — `nodeColor(node)` returns `#ff9800` (orange) when
+ the queue is between 80 % and 100 % full, `#d32f2f` (red) when fully
+ saturated. Overrides the user-defined `node.color`.
+- **Drop flash** — for ~200 ms after a node's `droppedCount` increments,
+ `nodeColor` returns `#ff1744` (bright red). Detected via diffing per
+ tick on `simulatorStats.queues[id].droppedCount`. The flash is short on
+ purpose so it doesn't merge into the sustained-saturation colour above.
+- **Role tint (only when no explicit `color`)** — `generator` → teal,
+ `sink` → indigo. `relay` keeps the renderer's default. Explicit DOT
+ colours are always preserved.
+
+Three module-level refs back this:
+`queueStatsByNodeRef`, `dropFlashTimeRef`, `previousDroppedCountRef`.
+They are updated by a `useEffect` listening to `simulatorStats`. The same
+effect re-installs the accessors (`fg.nodeVal(fg.nodeVal())`) so
+`3d-force-graph` re-evaluates them on every node — cheap (no layout
+rebuild), safe on large graphs.
+
+Don't read these refs from JSX directly — they are intentionally NOT
+React state because mutating them must not trigger a renderer rerender.
+A new `Drops` chip in the HUD reads from `simulatorStats` directly (which
+is React state), and is only rendered when `hasGenerators && simulatorStats`.
+
+---
+
+## 9. DOT parsing pipeline (with fallback)
**Primary path**: `DotTo3DConverter.parseDotToGraphData(dotContent)` calls
`${VITE_API_URL}/public/parse-dot` via `fetch` (currently — there's an open
@@ -189,6 +226,9 @@ Before merging a change to `GraphRenderer3D.tsx`, mentally walk through:
- [ ] "Émission particules" emits one particle per emitter and stops (doesn't
keep emitting).
- [ ] Stats panel shows non-zero values on a plain DOT graph (fallback works).
+- [ ] When a graph has `nodeRole=generator`, the DES simulator drives emission,
+ relay queues grow visibly under load, drops trigger a 200 ms red flash,
+ and the HUD shows a `Drops` chip.
- [ ] `setCurrentGraphData` is called from every init path you added.
- [ ] Test suite still passes (`npm test -- GraphRenderer3D.test.tsx`).
- [ ] Lint clean.
diff --git a/frontend/src/components/graphs/DotTo3DConverter.test.tsx b/frontend/src/components/graphs/DotTo3DConverter.test.tsx
index 7e52fb4..eb64a6f 100644
--- a/frontend/src/components/graphs/DotTo3DConverter.test.tsx
+++ b/frontend/src/components/graphs/DotTo3DConverter.test.tsx
@@ -222,9 +222,10 @@ describe('convertBackendDataToGraph', () => {
expect(r.nodes[0]).toEqual(expect.objectContaining({
id: 'A', name: 'Alpha', val: 10, color: '#ff0000', geometry: '3d-sphere',
}));
- // Defaults applied where backend omits values.
+ // Backend-omitted color stays undefined so the renderer's nodeColor
+ // accessor can apply role tints (ADR-006) before falling back to a default.
expect(r.nodes[1]).toEqual(expect.objectContaining({
- id: 'B', name: 'Beta', color: '#1976D2',
+ id: 'B', name: 'Beta', color: undefined,
}));
expect(r.links[0]).toEqual(expect.objectContaining({
source: 'A', target: 'B', name: 'link', color: '#00ff00', style: 'dashed',
diff --git a/frontend/src/components/graphs/GraphList.test.tsx b/frontend/src/components/graphs/GraphList.test.tsx
index e48e756..5d97b97 100644
--- a/frontend/src/components/graphs/GraphList.test.tsx
+++ b/frontend/src/components/graphs/GraphList.test.tsx
@@ -105,4 +105,42 @@ describe('GraphList', () => {
render();
await waitFor(() => expect(ctx.loadGraphs).toHaveBeenCalled());
});
+
+ // Regression: the menu item used to call handleMenuClose() which nulled
+ // selectedGraphId before the confirmation dialog could read it, so the
+ // dialog's "Supprimer" button became a no-op (no deleteGraph call).
+ test('Supprimer flow calls deleteGraph with the selected graph id', async () => {
+ const ctx = baseGraphCtx({
+ state: { graphs: [fakeGraph({ id: 42, name: 'Alpha' })] },
+ });
+ mockUseGraph.mockReturnValue(ctx);
+ const { container } = render();
+ // Open the per-card kebab menu (MoreVert button — only icon-only btn
+ // in the card with no accessible name).
+ const moreBtn = container.querySelector('[data-testid="MoreVertIcon"]')!.closest('button')!;
+ userEvent.click(moreBtn);
+ // Click "Supprimer" in the menu → opens the confirmation dialog.
+ userEvent.click(await screen.findByRole('menuitem', { name: /Supprimer/i }));
+ // Click the dialog's "Supprimer" button.
+ const dialog = await screen.findByRole('dialog');
+ userEvent.click(dialog.querySelector('button.MuiButton-containedError')! as HTMLElement);
+ await waitFor(() => expect(ctx.deleteGraph).toHaveBeenCalledWith(42));
+ });
+
+ // Regression (same root cause as above): Dupliquer was also broken.
+ test('Dupliquer flow calls duplicateGraph with the selected graph id', async () => {
+ const ctx = baseGraphCtx({
+ state: { graphs: [fakeGraph({ id: 99, name: 'Beta' })] },
+ });
+ mockUseGraph.mockReturnValue(ctx);
+ const { container } = render();
+ const moreBtn = container.querySelector('[data-testid="MoreVertIcon"]')!.closest('button')!;
+ userEvent.click(moreBtn);
+ userEvent.click(await screen.findByRole('menuitem', { name: /Dupliquer/i }));
+ const dialog = await screen.findByRole('dialog');
+ // The duplicate name input must be filled before the submit button is enabled.
+ userEvent.type(dialog.querySelector('input')!, 'Beta-copy');
+ userEvent.click(dialog.querySelector('button.MuiButton-containedPrimary')! as HTMLElement);
+ await waitFor(() => expect(ctx.duplicateGraph).toHaveBeenCalledWith(99, 'Beta-copy'));
+ });
});
diff --git a/frontend/src/components/graphs/GraphList.tsx b/frontend/src/components/graphs/GraphList.tsx
index 743e679..7ba7a5f 100644
--- a/frontend/src/components/graphs/GraphList.tsx
+++ b/frontend/src/components/graphs/GraphList.tsx
@@ -431,19 +431,24 @@ const GraphList: React.FC = () => {
)}
{canEdit() && (
-