@vscada/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/bin/vscada.js +58 -0
  3. package/dist/commands/preview.js +53 -0
  4. package/dist/scene-path.js +22 -0
  5. package/dist/scene-plugin.js +35 -0
  6. package/dist/template-registry.js +42 -0
  7. package/package.json +34 -0
  8. package/preview-app/index.html +55 -0
  9. package/preview-app/main.ts +149 -0
  10. package/preview-app/tsconfig.json +11 -0
  11. package/preview-app/vite-env.d.ts +10 -0
  12. package/templates/boiler-turbine-generator/README.md +9 -0
  13. package/templates/boiler-turbine-generator/scene.test.ts +26 -0
  14. package/templates/boiler-turbine-generator/scene.ts +91 -0
  15. package/templates/conveyor-batching/README.md +9 -0
  16. package/templates/conveyor-batching/scene.test.ts +30 -0
  17. package/templates/conveyor-batching/scene.ts +68 -0
  18. package/templates/hello-tank/README.md +18 -0
  19. package/templates/hello-tank/scene.test.ts +24 -0
  20. package/templates/hello-tank/scene.ts +50 -0
  21. package/templates/substation-single-line/README.md +9 -0
  22. package/templates/substation-single-line/scene.test.ts +31 -0
  23. package/templates/substation-single-line/scene.ts +63 -0
  24. package/templates/template-test-utils.ts +49 -0
  25. package/templates/tsconfig.json +11 -0
  26. package/templates/vitest.config.ts +11 -0
  27. package/templates/waste-to-energy/README.md +13 -0
  28. package/templates/waste-to-energy/scene.test.ts +15 -0
  29. package/templates/waste-to-energy/scene.ts +250 -0
  30. package/templates/waste-to-energy/wte-checklist.test.ts +105 -0
  31. package/templates/waste-to-energy/wte-topology.test.ts +38 -0
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { elementBindings } from '../template-test-utils';
3
+ import sceneBuilder from './scene';
4
+
5
+ /**
6
+ * Story 5.4 Task 5 — "the AC is a checklist, test it as one" (Dev Notes).
7
+ * Every `it` below is one bullet from base spec §16 (doc/SCADA-COMPONENT-SPEC.md),
8
+ * asserted as an item-by-item presence check against the built scene.
9
+ */
10
+ describe('waste-to-energy §16 checklist (Story 5.4 AC2)', () => {
11
+ const scene = sceneBuilder.build();
12
+ const elementsById = new Map(scene.elements.map((el) => [el.id, el]));
13
+ const typesById = new Map(scene.elements.map((el) => [el.id, el.type]));
14
+ const connectionsById = new Map(scene.connections.map((c) => [c.id, c]));
15
+
16
+ it('feed hopper (level) → furnace (state, firing rate)', () => {
17
+ expect(typesById.get('HPR-01')).toBe('hopper');
18
+ expect(elementBindings(elementsById.get('HPR-01')).some((b) => b.type === 'level')).toBe(true);
19
+ expect(typesById.get('FCE-01')).toBe('furnace');
20
+ expect(elementBindings(elementsById.get('FCE-01')).some((b) => b.type === 'state')).toBe(true);
21
+ expect(elementBindings(elementsById.get('FCE-01')).some((b) => b.type === 'color-ramp')).toBe(true); // firing rate
22
+ expect(connectionsById.get('C-WASTE')).toMatchObject({ from: 'HPR-01.outlet-bot', to: 'FCE-01.fuel-inlet' });
23
+ });
24
+
25
+ it('furnace → boiler drum via flue gas duct (gas train: furnace → duct → fans → coil bank → stack)', () => {
26
+ expect(typesById.get('DCT-01')).toBe('duct');
27
+ expect(connectionsById.get('C-FG1')).toMatchObject({ from: 'FCE-01.flue-outlet', to: 'DCT-01.inlet', medium: 'flue-gas' });
28
+ });
29
+
30
+ it('boiler drum (level, pressure) → turbine via steam line', () => {
31
+ expect(typesById.get('BLR-01')).toBe('boiler');
32
+ const boilerBindingTypes = elementBindings(elementsById.get('BLR-01')).map((b) => b.type);
33
+ expect(boilerBindingTypes).toContain('level');
34
+ expect(boilerBindingTypes).toContain('readout'); // pressure
35
+ expect(connectionsById.get('C-STM1')).toMatchObject({ from: 'BLR-01.steam-outlet', to: 'VLV-01.inlet', medium: 'steam' });
36
+ expect(connectionsById.get('C-STM2')).toMatchObject({ from: 'VLV-01.outlet', to: 'TRB-01.inlet', medium: 'steam' });
37
+ });
38
+
39
+ it('turbine (power, RPM readouts) → condenser → feedwater pump → back to drum (the recycle loop)', () => {
40
+ expect(typesById.get('TRB-01')).toBe('turbine');
41
+ const turbineBindingTypes = elementBindings(elementsById.get('TRB-01')).map((b) => b.type);
42
+ expect(turbineBindingTypes).toContain('readout'); // power
43
+ expect(turbineBindingTypes).toContain('rotate'); // rpm
44
+ expect(connectionsById.get('C-STM3')).toMatchObject({ from: 'TRB-01.outlet', to: 'CND-01.inlet' });
45
+ expect(connectionsById.get('C-COND')).toMatchObject({ from: 'CND-01.outlet', to: 'PMP-02.suction' });
46
+ expect(connectionsById.get('C-FW')).toMatchObject({ from: 'PMP-02.discharge', to: 'BLR-01.feedwater-inlet' }); // closes the loop
47
+ });
48
+
49
+ it('steam control valve (position 0–100) on the turbine inlet', () => {
50
+ expect(typesById.get('VLV-01')).toBe('valve-control');
51
+ const positionBinding = elementBindings(elementsById.get('VLV-01')).find((b) => b.type === 'position');
52
+ expect(positionBinding).toMatchObject({ min: 0, max: 100 });
53
+ });
54
+
55
+ it('two ID fans on the gas path', () => {
56
+ expect(typesById.get('FAN-01')).toBe('fan-centrifugal');
57
+ expect(typesById.get('FAN-02')).toBe('fan-centrifugal');
58
+ });
59
+
60
+ it('heat exchanger coil bank', () => {
61
+ expect(typesById.get('COIL-01')).toBe('heat-exchanger-coil');
62
+ });
63
+
64
+ it('stack with temperature readout and plume', () => {
65
+ expect(typesById.get('STK-01')).toBe('stack');
66
+ expect(elementBindings(elementsById.get('STK-01')).some((b) => b.type === 'readout')).toBe(true);
67
+ expect(typesById.get('PLM-01')).toBe('plume');
68
+ });
69
+
70
+ it('motor starter panels for each pump', () => {
71
+ const pumpIds = [...typesById.entries()].filter(([, type]) => type === 'pump-centrifugal' || type === 'pump-positive-displacement');
72
+ const panelIds = [...typesById.entries()].filter(([, type]) => type === 'motor-starter-panel');
73
+ expect(pumpIds.length).toBeGreaterThan(0);
74
+ expect(panelIds.length).toBe(pumpIds.length);
75
+ });
76
+
77
+ it('analog gauges for temperature and flow at four points', () => {
78
+ const gauges = [...typesById.entries()].filter(([, type]) => type === 'gauge-analog');
79
+ expect(gauges.length).toBe(4);
80
+ const gaugeTagUnits = gauges.map(([id]) => elementBindings(elementsById.get(id))[0]);
81
+ // At least one temperature-scaled gauge and one flow-scaled gauge, by convention of this template's own tag names.
82
+ expect(gaugeTagUnits.some((b) => b?.tag.includes('temp') || b?.tag.startsWith('FCE01') || b?.tag.startsWith('STK01'))).toBe(true);
83
+ expect(gaugeTagUnits.some((b) => b?.tag.includes('flow'))).toBe(true);
84
+ });
85
+
86
+ it('bottom status bar with plant-level readouts and a STOP button', () => {
87
+ expect(typesById.get('RDP-01')).toBe('readout-panel');
88
+ expect(typesById.get('PB-01')).toBe('pushbutton');
89
+ const group = scene.groups.find((g) => g.id === 'status-bar-group');
90
+ expect(group?.children).toEqual(expect.arrayContaining(['RDP-01', 'PB-01']));
91
+ });
92
+
93
+ it('media exercised: waste (solid), flue gas, steam, feedwater, condensate, ash', () => {
94
+ const media = new Set(scene.connections.map((c) => c.medium));
95
+ for (const expected of ['waste', 'flue-gas', 'steam', 'feedwater', 'condensate', 'ash']) {
96
+ expect(media.has(expected), `missing medium "${expected}"`).toBe(true);
97
+ }
98
+ });
99
+
100
+ it('boiler section is a collapsible group (Dev Notes)', () => {
101
+ const group = scene.groups.find((g) => g.id === 'boiler-section');
102
+ expect(group).toBeDefined();
103
+ expect(group?.children).toEqual(expect.arrayContaining(['BLR-01', 'VLV-01']));
104
+ });
105
+ });
@@ -0,0 +1,38 @@
1
+ import { buildTopologyGraph, downstreamOf } from '@vscada/core';
2
+ import { describe, expect, it } from 'vitest';
3
+ import sceneBuilder from './scene';
4
+
5
+ /**
6
+ * Story 5.4 Task 5 — "WtE topology cycle test": the recycle loop
7
+ * (boiler → valve → turbine → condenser → feedwater pump → boiler) must
8
+ * actually close, exercising Story 2.3's cycle-safe traversal (§16, AC2).
9
+ * `downstreamOf`/`reachableSet` always include the start node trivially
10
+ * (Story 2.3's own doc), so the real assertion walks from a DIRECT
11
+ * successor of the boiler and confirms the boiler is reachable again from
12
+ * there — a nontrivial cycle, not the traversal's own trivial self-membership.
13
+ */
14
+ describe('waste-to-energy topology (Story 5.4 AC2, Story 2.3 cycle safety)', () => {
15
+ const scene = sceneBuilder.build();
16
+ const graph = buildTopologyGraph(scene);
17
+
18
+ it('closes a recycle loop back onto the boiler drum', () => {
19
+ const boilerSuccessors = graph.outgoing.get('BLR-01') ?? [];
20
+ expect(boilerSuccessors.length).toBeGreaterThan(0);
21
+
22
+ const firstSuccessorId = boilerSuccessors[0]!.to;
23
+ const reachableFromSuccessor = downstreamOf(graph, firstSuccessorId);
24
+ expect(reachableFromSuccessor.has('BLR-01')).toBe(true);
25
+ });
26
+
27
+ it('the recycle loop passes through the turbine, condenser, and feedwater pump', () => {
28
+ const downstreamOfBoiler = downstreamOf(graph, 'BLR-01');
29
+ for (const id of ['VLV-01', 'TRB-01', 'CND-01', 'PMP-02']) {
30
+ expect(downstreamOfBoiler.has(id)).toBe(true);
31
+ }
32
+ });
33
+
34
+ it('does not throw or hang when traversing a graph that contains a cycle (the cycle-safety guard itself)', () => {
35
+ expect(() => downstreamOf(graph, 'BLR-01')).not.toThrow();
36
+ expect(() => downstreamOf(graph, 'FCE-01')).not.toThrow();
37
+ });
38
+ });