docs: standards cleanup — single front-door CONTRACTS.md + archive stale plan artifacts
Establish CONTRACTS.md at the EVOLV root as the canonical map of where every contract, rule, and standard lives. Surface it from CLAUDE.md so every fresh agent or colleague lands there first. Reshape .claude/refactor/ to reflect that the platform refactor is done: live standards stay at the top level; the plan artifacts (CONTINUE_HERE.md, TASKS.md) move into Archive/ with WARNING banners. Drop content that drifted out of date or duplicated the new standards stack: - docs/DEVELOPER_GUIDE.md (pre-refactor walkthrough; superseded by wiki/Architecture, wiki/Getting-Started, .claude/rules/node-architecture, .claude/refactor/MODULE_SPLIT + per-node CONTRACT.md + src/commands/). - .agents/decisions/ (15 DECISION files): load-bearing decisions belong in commit messages and PR descriptions; live open items in OPEN_QUESTIONS.md. - .agents/improvements/TOP10_*.md: moved to Archive/. Bump generalFunctions to 49c77f2 — adds CONTRACT.md inside the library: different shape from per-node CONTRACT.md files (library API, not msg.topic), with stability tags and pointers to .claude/refactor/CONTRACTS.md §N. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,17 +71,11 @@ Current owner-approved defaults (February 16, 2026):
|
||||
- Breaking `msg.topic`/payload changes are allowed only with explicit migration/deprecation notes.
|
||||
- Safety posture: `availability-first`
|
||||
- Prefer continuity of operation with bounded safeguards over early protective trips.
|
||||
- Decision logging: `required for all decision-gate changes`
|
||||
- Every decision-gate outcome must be recorded in `.agents/decisions/`.
|
||||
|
||||
Decision log:
|
||||
- Record important decisions in `.agents/decisions/DECISION-YYYYMMDD-<slug>.md`.
|
||||
- Include context, options, decision, consequences, and rollback/migration notes.
|
||||
- Decision logging: record load-bearing decisions in the commit message and PR description. Live open items belong in `.claude/refactor/OPEN_QUESTIONS.md`. Superseded plan artifacts live in `.agents/improvements/Archive/` and `.claude/refactor/Archive/`.
|
||||
|
||||
Functional/architectural improvements backlog:
|
||||
- Track deferred functional/runtime/architecture improvements in `.agents/improvements/IMPROVEMENTS_BACKLOG.md`.
|
||||
- If an improvement is discovered during non-functional work, add it to this backlog before closing the task.
|
||||
- Keep the top priority review list in `.agents/improvements/TOP10_PRODUCTION_PRIORITIES_YYYY-MM-DD.md` when requested.
|
||||
- When an item is implemented after review, remove it from `.agents/improvements/IMPROVEMENTS_BACKLOG.md` and note the fix in session notes/PR context.
|
||||
|
||||
## Agent Routing Table
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# DECISION-20260216-agent-harness-defaults
|
||||
|
||||
## Context
|
||||
- Task/request: Adapt EVOLV agents/skills using Harness Engineering patterns and set owner-controlled operating defaults.
|
||||
- Impacted files/contracts: `.agents/AGENTS.md`, `.agents/skills/*/SKILL.md`, `.agents/skills/*/agents/openai.yaml`, decision-log policy.
|
||||
- Why a decision is required now: New harness workflow needs explicit defaults for compatibility, safety bias, and governance discipline.
|
||||
|
||||
## Options
|
||||
1. Compatibility posture
|
||||
- Option A: strict backward compatibility
|
||||
- Option B: controlled compatibility breaks with migration notes
|
||||
|
||||
2. Safety posture
|
||||
- Option A: protection-first
|
||||
- Option B: availability-first
|
||||
|
||||
3. Decision logging scope
|
||||
- Option A: required only for breaking/risky changes
|
||||
- Option B: required for all decision-gate outcomes
|
||||
|
||||
## Decision
|
||||
- Selected option: Compatibility `controlled`; Safety `availability-first`; Decision logging `required for all decision-gate changes`.
|
||||
- Decision owner: User
|
||||
- Date: February 16, 2026
|
||||
- Rationale: Maintain delivery and operational continuity while preserving governance through mandatory, durable decision records.
|
||||
|
||||
## Consequences
|
||||
- Compatibility impact: Breaking contract changes are permissible only when migration/deprecation is explicit.
|
||||
- Safety/security impact: Control changes should bias toward continuity with bounded safeguards; critical protections still require explicit constraints.
|
||||
- Data/operations impact: Decision traceability improves cross-turn consistency and auditability.
|
||||
|
||||
## Implementation Notes
|
||||
- Required code/doc updates: Set defaults in `.agents/AGENTS.md` and orchestrator skill instructions; keep decision-log template active.
|
||||
- Validation evidence required: Presence of defaults in policy docs and this decision artifact under `.agents/decisions/`.
|
||||
|
||||
## Rollback / Migration
|
||||
- Rollback strategy: Update defaults in `.agents/AGENTS.md` and orchestrator SKILL; create a superseding decision log entry.
|
||||
- Migration/deprecation plan: For any future hard-break preference, require explicit migration plan and effective date in a new decision entry.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Decision: Shared Modern PID in generalFunctions + PumpingStation Flow-Based Adoption
|
||||
|
||||
- Date: 2026-02-23
|
||||
- Scope: `nodes/generalFunctions/src/pid/*`, `nodes/pumpingStation/src/*`
|
||||
|
||||
## Context
|
||||
Flow-based control in `pumpingStation` needed a production-grade PID with freeze/unfreeze, runtime retuning, and support for cascade/secondary-loop architecture.
|
||||
|
||||
## Options Considered
|
||||
1. Implement PID only inside `pumpingStation`.
|
||||
2. Implement shared PID in `generalFunctions` and consume it from `pumpingStation`.
|
||||
3. Keep current heuristic (non-PID) flow controller.
|
||||
|
||||
## Decision
|
||||
Chose option 2.
|
||||
|
||||
## Rationale
|
||||
- PID behavior is cross-domain control functionality and should be reusable across EVOLV nodes.
|
||||
- `generalFunctions` already serves as shared utility/runtime infrastructure.
|
||||
- Reuse reduces drift and duplicated control logic.
|
||||
- PumpingStation can immediately adopt shared PID while preserving existing topic contracts.
|
||||
|
||||
## Consequences
|
||||
- Positive:
|
||||
- Single, test-covered PID implementation with modern features.
|
||||
- PumpingStation flow mode becomes true closed-loop control.
|
||||
- Runtime support for freeze/unfreeze and tuning updates without redeploy.
|
||||
- Risks:
|
||||
- Behavioral differences versus prior heuristic flow control.
|
||||
- Requires conservative tuning per site.
|
||||
|
||||
## Safety / Compatibility
|
||||
- No existing topic names were removed.
|
||||
- Added optional control topics for PID runtime management.
|
||||
- Existing non-flowbased modes remain intact.
|
||||
|
||||
## Rollback
|
||||
- Revert `nodes/pumpingStation/src/specificClass.js` flow-based branch to previous heuristic logic.
|
||||
- Keep shared PID module in `generalFunctions` for future use, or revert `nodes/generalFunctions/src/pid/*` if required.
|
||||
|
||||
## Migration Notes
|
||||
- For `flowbased`, start with low `kp/ki`, verify stability in commissioning, then tune upward.
|
||||
- Use `freezeFlowPid` and `setFlowPidMode` during maintenance or manual takeover.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Decision: Harden NRMSE and Use Metric Profiles in RotatingMachine
|
||||
|
||||
- Date: 2026-02-24
|
||||
- Scope: `nodes/generalFunctions/src/nrmse/*`, `nodes/rotatingMachine/src/specificClass.js`
|
||||
|
||||
## Context
|
||||
Drift analytics were previously single-path and flow-focused with weak input safeguards in NRMSE.
|
||||
Requirement: make NRMSE architecturally robust and apply it across multiple measurements in rotatingMachine.
|
||||
|
||||
## Decision
|
||||
Adopt a metric-profile drift architecture:
|
||||
|
||||
1. Harden `generalFunctions/nrmse` with:
|
||||
- strict validation for malformed inputs
|
||||
- timestamp-aware alignment support
|
||||
- per-metric state
|
||||
- configurable rolling window and EWMA long-term trend
|
||||
- point-based API (`assessPoint`) while retaining legacy calls
|
||||
|
||||
2. Rewire rotatingMachine to consume NRMSE per metric:
|
||||
- `flow` model drift
|
||||
- `power` model drift
|
||||
- pressure-quality drift as node-specific plausibility/redundancy assessment
|
||||
|
||||
3. Expose drift and confidence outputs per metric in node output payload.
|
||||
|
||||
## Consequences
|
||||
- Drift computations are deterministic and safer under bad inputs.
|
||||
- RotatingMachine confidence now reflects multiple measurement channels.
|
||||
- Output schema expands with power/pressure drift fields.
|
||||
|
||||
## Rollback Notes
|
||||
- Revert `errorMetrics.js` and rotatingMachine drift wiring to return to legacy flow-only drift behavior.
|
||||
@@ -1,34 +0,0 @@
|
||||
# Decision: RotatingMachine Hydraulic Efficiency Correction and Prediction Confidence
|
||||
|
||||
- Date: 2026-02-24
|
||||
- Scope: `nodes/rotatingMachine/src/specificClass.js`, rotatingMachine integration tests
|
||||
|
||||
## Context
|
||||
Hydraulic efficiency calculation in `rotatingMachine` was dimensionally inconsistent and could over/under-report efficiency KPIs.
|
||||
At the same time, prediction drift tooling (`nrmse`) existed but was not actively connected to rotatingMachine output confidence.
|
||||
|
||||
## Options Considered
|
||||
1. Keep existing formula and only tune thresholds.
|
||||
2. Replace formula with standard hydraulic power/efficiency equations and expose prediction confidence from live pressure source + drift.
|
||||
|
||||
## Decision
|
||||
Adopt option 2.
|
||||
|
||||
- Hydraulic power now follows standard engineering relation:
|
||||
- `P_h = Q * Δp` (equivalent to `ρ g Q H`)
|
||||
- `η_h = P_h / P_in`
|
||||
- RotatingMachine now computes flow drift via `nrmse` from measured vs predicted flow windows.
|
||||
- RotatingMachine now exposes prediction confidence fields in output:
|
||||
- `predictionQuality`
|
||||
- `predictionConfidence`
|
||||
- `predictionPressureSource`
|
||||
- `predictionFlags`
|
||||
|
||||
## Consequences
|
||||
- Efficiency KPIs become physically interpretable and traceable to pressure/flow/power inputs.
|
||||
- Prediction trust is now observable by downstream control/dashboard layers.
|
||||
- Output schema is expanded with new prediction confidence fields.
|
||||
|
||||
## Rollback / Migration Notes
|
||||
- Rollback path: revert `specificClass.js` hydraulic block and prediction-health integration.
|
||||
- No mandatory migration required for existing flows unless they choose to consume new prediction confidence fields.
|
||||
@@ -1,38 +0,0 @@
|
||||
# Decision: Canonical Unit Anchoring and Curve Unit Normalization in RotatingMachine
|
||||
|
||||
- Date: 2026-02-24
|
||||
- Scope: `nodes/rotatingMachine/*`, `nodes/generalFunctions/src/measurements/MeasurementContainer.js`, `nodes/generalFunctions/src/configs/rotatingMachine.json`
|
||||
|
||||
## Context
|
||||
RotatingMachine previously relied on node-local defaults for measurement storage units, with implicit assumptions that loaded machine curves used the same units as runtime configuration. This made unit drift likely when model curves, simulated inputs, and runtime settings differed.
|
||||
|
||||
Owner decision direction:
|
||||
- use a single unit anchor strategy
|
||||
- treat node/UI units as ingress/egress only
|
||||
- add explicit curve unit metadata
|
||||
- reject or flag blank/invalid measurement units
|
||||
|
||||
## Decision
|
||||
1. Extend `MeasurementContainer` with optional canonical-anchor mode:
|
||||
- per-type canonical unit mapping
|
||||
- strict unit validation and required-unit policy
|
||||
- compatibility checks by measure family
|
||||
- requested-unit conversion at flattened output stage
|
||||
|
||||
2. Apply canonical policy in `rotatingMachine` runtime:
|
||||
- internal storage and calculations anchored to SI-like canonical units (`Pa`, `m3/s`, `W`, `K`)
|
||||
- egress payloads converted back to configured output units
|
||||
- ingress `simulateMeasurement` path requires explicit valid units
|
||||
|
||||
3. Add explicit curve unit metadata (`asset.curveUnits`) and normalize loaded curves into canonical units before predictor initialization.
|
||||
|
||||
## Consequences
|
||||
- Unit handling is centralized and deterministic for RotatingMachine.
|
||||
- Curve/model-unit mismatch risk is reduced by explicit metadata plus normalization.
|
||||
- Existing output topic/field names remain stable; values are emitted in configured output units while internals stay canonical.
|
||||
- This establishes a migration template for remaining EVOLV nodes.
|
||||
|
||||
## Rollback Notes
|
||||
- Revert `MeasurementContainer` canonical/validation extensions.
|
||||
- Revert RotatingMachine unit-policy and curve-normalization wiring.
|
||||
- Remove `asset.curveUnits` schema entry and restore previous node-local default-unit behavior.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Decision: Unit-Anchor Rollout Phase 1 (MachineGroup, PumpingStation, Valve, ValveGroupControl)
|
||||
|
||||
- Date: 2026-02-24
|
||||
- Scope:
|
||||
- `nodes/machineGroupControl/src/nodeClass.js`
|
||||
- `nodes/machineGroupControl/src/specificClass.js`
|
||||
- `nodes/pumpingStation/src/nodeClass.js`
|
||||
- `nodes/pumpingStation/src/specificClass.js`
|
||||
- `nodes/valve/src/nodeClass.js`
|
||||
- `nodes/valve/src/specificClass.js`
|
||||
- `nodes/valveGroupControl/src/nodeClass.js`
|
||||
- `nodes/valveGroupControl/src/specificClass.js`
|
||||
|
||||
## Context
|
||||
After adopting canonical-unit anchoring in `rotatingMachine`, adjacent controller nodes still mixed local units, unitless writes, and implicit conversions. That left cross-node behavior sensitive to registration order and source-unit assumptions.
|
||||
|
||||
## Decision
|
||||
1. Apply the same canonical storage policy per node:
|
||||
- internal storage in canonical units (`Pa`, `m3/s`, `W`, `K` where relevant),
|
||||
- preferred/output units for operator-facing status and output payloads.
|
||||
|
||||
2. Enable strict measurement ingress discipline on migrated nodes:
|
||||
- `strictUnitValidation: true`,
|
||||
- `throwOnInvalidUnit: true`,
|
||||
- required unit for physically dimensional types (`flow`, `pressure`, `power`, `temperature`, and node-specific equivalents).
|
||||
|
||||
3. Replace unitless runtime writes/reads with explicit-unit helpers in each node’s domain class, including child-machine/child-valve interactions.
|
||||
|
||||
## Consequences
|
||||
- Cross-node calculations now run against a deterministic unit anchor in phase-1 nodes.
|
||||
- Status/output values remain in preferred/output units, while internal math stays canonical.
|
||||
- Legacy paths that send dimensional values without units now fail fast instead of silently coercing.
|
||||
|
||||
## Rollback Notes
|
||||
- Revert the eight files listed in scope.
|
||||
- Restore previous `MeasurementContainer` initialization (non-canonical, non-strict behavior) in each node.
|
||||
- Remove helper-based explicit unit reads/writes and revert to prior direct chain usage.
|
||||
@@ -1,36 +0,0 @@
|
||||
# DECISION-20260323-architecture-layering-resilience-and-config-authority
|
||||
|
||||
## Context
|
||||
- Task/request: refine the EVOLV architecture baseline using the current stack drawings and owner guidance.
|
||||
- Impacted files/contracts: architecture documentation, future wiki structure, telemetry/storage strategy, security boundaries, and configuration authority assumptions.
|
||||
- Why a decision is required now: the architecture can no longer stay at a generic "Node-RED plus cloud" level; several operating principles were clarified by the owner and need to be treated as architectural defaults.
|
||||
|
||||
## Options
|
||||
1. Keep the architecture intentionally broad and tool-centric
|
||||
- Benefits: fewer early commitments.
|
||||
- Risks: blurred boundaries for resilience, data ownership, and security; easier to drift into contradictory implementations.
|
||||
- Rollout notes: wiki remains descriptive but not decision-shaping.
|
||||
|
||||
2. Adopt explicit defaults for resilience, API boundary, telemetry layering, and configuration authority
|
||||
- Benefits: clearer target operating model; easier to design stack services and wiki pages consistently; aligns diagrams with intended operational behavior.
|
||||
- Risks: some assumptions may outpace current implementation and therefore create an architecture debt backlog.
|
||||
- Rollout notes: document gaps clearly and treat incomplete systems as planned workstreams rather than pretending they already exist.
|
||||
|
||||
## Decision
|
||||
- Selected option: Option 2.
|
||||
- Decision owner: repository owner confirmed during architecture review.
|
||||
- Date: 2026-03-23.
|
||||
- Rationale: the owner clarified concrete architecture goals that materially affect security, resilience, and platform structure. The documentation should encode those as defaults instead of leaving them implicit.
|
||||
|
||||
## Consequences
|
||||
- Compatibility impact: low immediate code impact, but future implementations should align to these defaults.
|
||||
- Safety/security impact: improved boundary clarity by making central the integration entry point and keeping edge protected behind site/central mediation.
|
||||
- Data/operations impact: multi-level InfluxDB and smart-storage behavior become first-class design concerns; `tagcodering` becomes the intended configuration backbone.
|
||||
|
||||
## Implementation Notes
|
||||
- Required code/doc updates: update the architecture review doc, add visual wiki-ready diagrams, and track follow-up work for incomplete `tagcodering` integration and telemetry policy design.
|
||||
- Validation evidence required: architecture docs reflect the agreed principles and diagrams; no contradiction with current repo evidence for implemented components.
|
||||
|
||||
## Rollback / Migration
|
||||
- Rollback strategy: return to a generic descriptive architecture document without explicit defaults.
|
||||
- Migration/deprecation plan: implement these principles incrementally, starting with configuration authority, telemetry policy, and site/central API boundaries.
|
||||
@@ -1,36 +0,0 @@
|
||||
# DECISION-20260323-compose-secrets-via-env
|
||||
|
||||
## Context
|
||||
- Task/request: harden the target-state stack example so credentials are not stored directly in `temp/cloud.yml`.
|
||||
- Impacted files/contracts: `temp/cloud.yml`, deployment/operations practice for target-state infrastructure examples.
|
||||
- Why a decision is required now: the repository contained inline credentials in a tracked compose file, which conflicts with the intended security posture and creates avoidable secret-leak risk.
|
||||
|
||||
## Options
|
||||
1. Keep credentials inline in the compose file
|
||||
- Benefits: simplest to run as a standalone example.
|
||||
- Risks: secrets leak into git history, reviews, copies, and local machines; encourages unsafe operational practice.
|
||||
- Rollout notes: none, but the risk remains permanent once committed.
|
||||
|
||||
2. Move credentials to server-side environment variables and keep only placeholders in compose
|
||||
- Benefits: aligns the manifest with a safer deployment pattern; keeps tracked config portable across environments; supports secret rotation without editing the compose file.
|
||||
- Risks: operators must manage `.env` or equivalent secret injection correctly.
|
||||
- Rollout notes: provide an example env file and document that the real `.env` stays on the server and out of version control.
|
||||
|
||||
## Decision
|
||||
- Selected option: Option 2.
|
||||
- Decision owner: repository owner confirmed during task discussion.
|
||||
- Date: 2026-03-23.
|
||||
- Rationale: the target architecture should model the right operational pattern. Inline secrets in repository-tracked compose files are not acceptable for EVOLV's intended OT/IT deployment posture.
|
||||
|
||||
## Consequences
|
||||
- Compatibility impact: low; operators now need to supply environment variables when deploying `temp/cloud.yml`.
|
||||
- Safety/security impact: improved secret hygiene and lower credential exposure risk.
|
||||
- Data/operations impact: deployment requires an accompanying `.env` on the server or explicit `--env-file` usage.
|
||||
|
||||
## Implementation Notes
|
||||
- Required code/doc updates: replace inline secrets in `temp/cloud.yml`; add `temp/cloud.env.example`; keep the real `.env` untracked on the server.
|
||||
- Validation evidence required: inspect compose file for `${...}` placeholders and verify no real credentials remain in tracked files touched by this change.
|
||||
|
||||
## Rollback / Migration
|
||||
- Rollback strategy: reintroduce inline values, though this is not recommended.
|
||||
- Migration/deprecation plan: create a server-local `.env` from `temp/cloud.env.example`, fill in real values, and run compose from that environment.
|
||||
@@ -1,43 +0,0 @@
|
||||
## Context
|
||||
|
||||
The single demo bioreactor did not reflect the intended EVOLV biological treatment concept. The owner requested:
|
||||
|
||||
- four reactor zones in series
|
||||
- staged aeration based on effluent NH4
|
||||
- local visualization per zone for NH4, NO3, O2, and other relevant state variables
|
||||
- improved PFR numerical stability by increasing reactor resolution
|
||||
|
||||
The localhost deployment also needed to remain usable for E2E debugging with Node-RED, InfluxDB, and Grafana.
|
||||
|
||||
## Options Considered
|
||||
|
||||
1. Keep one large PFR and add more internal profile visualization only.
|
||||
2. Split the biology into four explicit reactor zones in the flow and control aeration at zone level.
|
||||
3. Replace the PFR demo with a simpler CSTR train for faster visual response.
|
||||
|
||||
## Decision
|
||||
|
||||
Choose option 2.
|
||||
|
||||
The demo flow now uses four explicit PFR zones in series with:
|
||||
|
||||
- equal-zone sizing (`4 x 500 m3`, total `2000 m3`)
|
||||
- explicit `Fluent` forwarding between zones
|
||||
- common clocking for all zones
|
||||
- external `OTR` control instead of fixed `kla`
|
||||
- staged NH4-based aeration escalation with 30-minute hold logic
|
||||
- per-zone telemetry to InfluxDB and Node-RED dashboard charts
|
||||
|
||||
For runtime stability on localhost, the demo uses a higher spatial resolution with moderate compute load rather than the earlier single-reactor setup.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The flow is easier to reason about operationally because each aeration zone is explicit.
|
||||
- Zone-level telemetry is available for dashboarding and debugging.
|
||||
- PFR outlet response remains residence-time dependent, so zone outlet composition will not change instantly after startup or inflow changes.
|
||||
- Grafana datasource query round-trip remains valid, but dashboard auto-generation still needs separate follow-up if strict dashboard creation is required in E2E checks.
|
||||
|
||||
## Rollback / Migration Notes
|
||||
|
||||
- Rolling back to the earlier demo means restoring the single `demo_reactor` topology in `docker/demo-flow.json`.
|
||||
- Existing E2E checks and dashboards should prefer the explicit zone measurements (`reactor_demo_reactor_z1` ... `reactor_demo_reactor_z4`) going forward.
|
||||
@@ -1,54 +0,0 @@
|
||||
# DECISION-20260422-pumpingstation-5-threshold-naming
|
||||
|
||||
## Context
|
||||
- Task/request: Re-draw the pumpingStation basin model and rename the configuration fields so they match the conceptual model used in the wiki.
|
||||
- Impacted files/contracts:
|
||||
- `generalFunctions/src/configs/pumpingStation.json` (schema keys)
|
||||
- `nodes/pumpingStation/src/specificClass.js` (internal state + comments)
|
||||
- `nodes/pumpingStation/src/nodeClass.js` (config ingestion mapping)
|
||||
- `nodes/pumpingStation/pumpingStation.html` (editor field IDs + labels)
|
||||
- `nodes/pumpingStation/test/*` (test fixtures)
|
||||
- `examples/pumpingstation-3pumps-dashboard/{build_flow.py, flow.json, README.md}`
|
||||
- saved production flows that reference the old field names (breaking change)
|
||||
- Why a decision is required now: The old names (`stopLevel`, `maxFlowLevel`, `minFlowLevel`, `heightInlet/Outlet/Overflow`) conflated geometry with control thresholds and had a redundant field (`minFlowLevel` always had to equal `startLevel`).
|
||||
|
||||
## Options
|
||||
1. Keep old names; just document them better
|
||||
- Benefits: Zero breaking change.
|
||||
- Risks: Naming keeps confusing new contributors; docs continue drifting from code.
|
||||
|
||||
2. Adopt the 5-threshold naming from the wiki basin diagram (selected)
|
||||
- Benefits: Clear semantic split — two safety thresholds (`dryRunLevel`, `overflowLevel`), three control thresholds (`minLevel`, `startLevel`, `maxLevel`) — plus three physical pipe heights (`inflowLevel`, `outflowLevel`, basin `height`). Drops the redundant `minFlowLevel`. Matches the diagram in the functional description.
|
||||
- Risks: Breaking change for saved flows; node editor fields must be re-entered.
|
||||
- Rollout notes: RnD/trial node — no compat shim. Breaking change documented in commit bodies and wiki.
|
||||
|
||||
## Decision
|
||||
- Selected option: Option 2.
|
||||
- Decision owner: User
|
||||
- Date: 2026-04-22
|
||||
- Rationale: The names should reflect the model. The diagram came first; the code should match the diagram, not the other way around. Compat posture is "controlled" (per DECISION-20260216) — breaking changes are permitted with migration notes.
|
||||
|
||||
## Mapping
|
||||
| Old | New |
|
||||
|---|---|
|
||||
| `heightInlet` | `inflowLevel` |
|
||||
| `heightOutlet` | `outflowLevel` |
|
||||
| `heightOverflow` | `overflowLevel` |
|
||||
| `stopLevel` | `minLevel` |
|
||||
| `maxFlowLevel` | `maxLevel` |
|
||||
| `minFlowLevel` | removed (collapsed into `startLevel`) |
|
||||
| `minVolIn/Out` (internal) | `minVolAtInflow/Outflow` |
|
||||
| `maxVolOverflow` (internal) | `maxVolAtOverflow` |
|
||||
|
||||
## Consequences
|
||||
- Compatibility impact: Existing flows break; editor fields must be re-entered.
|
||||
- Safety/security impact: Safety thresholds (`dryRunLevel`, `overflowLevel`) now have first-class names — guardrail validation can reason about them explicitly.
|
||||
- Data/operations impact: InfluxDB payload field names change (`maxVolOverflow` → `maxVolAtOverflow` etc.). Downstream Grafana dashboards referencing the old names must update.
|
||||
|
||||
## Implementation Notes
|
||||
- Required code/doc updates: Done in commits pumpingStation@a218945, generalFunctions@4252292, EVOLV@b885f29.
|
||||
- Validation evidence required: Unit tests (`node --test test/basic/*.test.js`) pass; `grep -r` confirms zero residual old names in pumpingStation/ + generalFunctions/pumpingStation.json + examples/.
|
||||
|
||||
## Rollback / Migration
|
||||
- Rollback strategy: revert the three commits; the renames are isolated.
|
||||
- Migration/deprecation plan: None — RnD node, breaking change is acceptable.
|
||||
@@ -1,46 +0,0 @@
|
||||
# DECISION-20260422-pumpingstation-mode-tier-template
|
||||
|
||||
## Context
|
||||
- Task/request: Document each pumpingStation control mode uniformly so operators can compare them and contributors can add new ones from a template.
|
||||
- Impacted files/contracts: `wiki/modes/*.md`, `wiki/diagrams/modes/*.drawio.svg`.
|
||||
- Why a decision is required now: The initial `levelbased.md` used a 2D `demand-vs-level` transfer-function plot. That plot form works for static memoryless control but misleads for modes whose curve shape changes at runtime (e.g. `powerBased`) or where there is no curve at all (`mpc`). We need one template that stretches to cover all cases.
|
||||
|
||||
## Options
|
||||
1. One template, transfer-function only
|
||||
- Benefits: Uniformity.
|
||||
- Risks: Silently misleading for Tier-2/Tier-3 modes where the "curve" is not well-defined.
|
||||
|
||||
2. Per-mode ad-hoc diagrams
|
||||
- Benefits: Each mode gets the best visual for itself.
|
||||
- Risks: No common vocabulary — comparing modes becomes harder.
|
||||
|
||||
3. Three-tier template (selected)
|
||||
- Benefits: Classifies every mode into one of three buckets, each with a dedicated diagram type. Still one template — only the diagram section branches.
|
||||
- Risks: Some modes don't fit cleanly; will need judgement.
|
||||
|
||||
## Tier definitions
|
||||
|
||||
| Tier | Control surface | Example modes | Diagrams |
|
||||
|---|---|---|---|
|
||||
| 1 | Static: `demand = f(x)` memoryless | `levelbased`, `manual` | Single-curve transfer function |
|
||||
| 2 | Parameterised: shape fixed, curve moves with `θ(t)` | `flowbased` (PID), `pressureBased`, `percentageBased`, `powerBased` | Transfer function + parameter-overlay / family-of-curves |
|
||||
| 3 | Optimisation / horizon: no fixed curve | `hybrid-optimal`, `mpc`, weather-aware | Block diagram of signal flow + scenario time-series |
|
||||
|
||||
## Decision
|
||||
- Selected option: Option 3 — three-tier classification with diagram type per tier.
|
||||
- Decision owner: User
|
||||
- Date: 2026-04-22
|
||||
- Rationale: Keeps the mode pages comparable (same six sections) while being honest about what's actually drawable. Tier-3 modes get scenario-based analysis (via the `eval/` harness) instead of a fictitious static curve.
|
||||
|
||||
## Consequences
|
||||
- Compatibility impact: None — this is doc-level.
|
||||
- Safety/security impact: None.
|
||||
- Data/operations impact: New modes get a template to follow; reviews have a shared vocabulary.
|
||||
|
||||
## Implementation Notes
|
||||
- Required code/doc updates: `wiki/modes/README.md` lists the tiers and template; `wiki/modes/{flowbased, powerbased, mpc}.md` are worked templates covering Tier 2 (×2) and Tier 3 (×1) respectively.
|
||||
- Validation evidence required: A reviewer reading a mode page can identify which tier it is within 10 seconds without scrolling.
|
||||
|
||||
## Rollback / Migration
|
||||
- Rollback strategy: Delete `wiki/modes/`; revert the table in `wiki/README.md`.
|
||||
- Migration/deprecation plan: N/A — adding a tier later (e.g. Tier 4 — RL-based) is trivially additive.
|
||||
@@ -1,57 +0,0 @@
|
||||
# DECISION-20260422-pumpingstation-simulations-harness
|
||||
|
||||
## Context
|
||||
- Task/request: Provide a way to fluctuate inputs to the pumpingStation and observe the system's response over time, in a readable form suitable for post-hoc analysis (operator review, Grafana, or ad-hoc debugging).
|
||||
- Impacted files/contracts: `nodes/pumpingStation/simulations/*`, `test/basic/*`.
|
||||
- Why a decision is required now: Unit tests (`node --test`) verify individual functions in isolation. They can't ergonomically show "what does the level look like over 20 minutes of storm surge". That's a different artefact.
|
||||
|
||||
## Options
|
||||
1. Extend unit tests to cover scenarios
|
||||
- Benefits: Single testing surface.
|
||||
- Risks: Unit tests are assertion-heavy and slow to read; scenario output (tables, events) gets lost in TAP.
|
||||
|
||||
2. Separate `simulations/` folder with a scenario runner (selected)
|
||||
- Benefits: Scenarios read as narratives ("steady state", "storm surge", "safety dry-run"); output is human-friendly (ASCII table + events + expectation checks); JSONL per-tick log enables Grafana streaming or offline analysis.
|
||||
- Risks: Second test surface to maintain.
|
||||
|
||||
3. Real-time Node-RED deployment + observe
|
||||
- Benefits: Closest to production.
|
||||
- Risks: Slow, requires infrastructure, irreproducible.
|
||||
|
||||
## Decision
|
||||
- Selected option: Option 2.
|
||||
- Decision owner: User
|
||||
- Date: 2026-04-22
|
||||
- Rationale: Unit tests answer "is this function correct?"; scenarios answer "how does the system behave under this input profile?". Two distinct questions — two distinct tools. The split also matches the .claude/rules/testing.md 3-tier convention (basic/integration/edge) which is for asserted behaviours, not scenario replay.
|
||||
|
||||
### Addendum (same-day rename)
|
||||
|
||||
Folder was initially named `eval/`. Renamed to `simulations/` in commit pumpingStation@3e13512 — `eval` and `test` are near-synonyms so the split implied a conceptual difference that doesn't really exist. `simulations/` is more honest about what's happening (scripted plant inputs driving a physics sim, recorded for analysis). Rationale above is unchanged; only the folder name is.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
test/
|
||||
basic/ integration/ edge/ — node:test + assertions
|
||||
simulations/
|
||||
run.js — scenario driver
|
||||
scenarios/*.js — each exports { name, config, setup, inputs(t,ps), expectations }
|
||||
formatters/table.js — ASCII summary
|
||||
logs/*.jsonl — one-line-per-tick output
|
||||
README.md — usage + how to pipe into Grafana
|
||||
```
|
||||
|
||||
Driver monkey-patches `Date.now()` so the volume integrator sees 1 second per tick regardless of wall-clock. Every tick records a state snapshot (level, volume, direction, netFlow, flowSource, demand, mode, safetyActive) to JSONL for streaming.
|
||||
|
||||
## Consequences
|
||||
- Compatibility impact: None.
|
||||
- Safety/security impact: None — read-only simulation.
|
||||
- Data/operations impact: Running `node simulations/run.js --all` produces artefacts that can be checked into CI for regression (e.g. "did the storm scenario's max level rise compared to last release?"). The JSONL format is friendly to InfluxDB/Grafana for interactive review.
|
||||
|
||||
## Implementation Notes
|
||||
- Required code/doc updates: Driver + three starter scenarios (`levelbased-steady`, `levelbased-storm`, `safety-dry-run-trip`) + README in `simulations/`.
|
||||
- Validation evidence required: `node simulations/run.js --all` exits 0; manual inspection of JSONL confirms per-tick records make physical sense.
|
||||
|
||||
## Rollback / Migration
|
||||
- Rollback strategy: Delete `simulations/`. Unit tests continue to work.
|
||||
- Migration/deprecation plan: N/A.
|
||||
@@ -1,41 +0,0 @@
|
||||
# DECISION-20260422-pumpingstation-wiki-in-code-repo
|
||||
|
||||
## Context
|
||||
- Task/request: Document pumpingStation functional behaviour (basin model, control modes, safety). Initial draft went into the Gitea wiki repo (`pumpingStation.wiki.git`).
|
||||
- Impacted files/contracts: location of all pumpingStation documentation; how docs, diagrams, and code stay in sync; wiki UI vs repo browsing UX.
|
||||
- Why a decision is required now: Wiki repo + code repo diverge silently. When `specificClass.js` renames a field, nothing forces the wiki to follow. User preference is "single package" — clone once, edit together, review together.
|
||||
|
||||
## Options
|
||||
1. Keep docs in `pumpingStation.wiki.git` (Gitea's native wiki)
|
||||
- Benefits: Gitea wiki UI (Pages dropdown, `?edit=1`, dedicated URL).
|
||||
- Risks: Two separate repos; code and doc drift silently.
|
||||
- Rollout notes: Status quo as of 2026-04-22.
|
||||
|
||||
2. Move docs + diagrams into `pumpingStation.git/wiki/` (selected)
|
||||
- Benefits: Single package — `git clone pumpingStation` gets code + docs + diagrams. Atomic commits can change code + doc + diagram together. Diagrams version-lock with the class they describe.
|
||||
- Risks: Lose the Gitea wiki Pages dropdown. Browsing is via the repo tree.
|
||||
- Rollout notes: Shrink the `.wiki.git` to a pointer at the new location.
|
||||
|
||||
3. Hybrid — diagrams only in code repo, Markdown pages in `.wiki.git`
|
||||
- Benefits: Keep Gitea wiki UI.
|
||||
- Risks: Image URLs break silently on rename; still two repos to sync.
|
||||
- Rollout notes: Not pursued.
|
||||
|
||||
## Decision
|
||||
- Selected option: Option 2 — everything under `pumpingStation/wiki/`.
|
||||
- Decision owner: User (r.de.ren@brabantsedelta.nl)
|
||||
- Date: 2026-04-22
|
||||
- Rationale: Single package > Gitea wiki UI convenience. Review-as-one-PR pattern is worth more than the Pages dropdown. `wiki/README.md` acts as the index instead.
|
||||
|
||||
## Consequences
|
||||
- Compatibility impact: Anyone bookmarking `RnD/pumpingStation/wiki/Functional-Description` lands on a one-line pointer. Breaking but low-impact.
|
||||
- Safety/security impact: None.
|
||||
- Data/operations impact: Future contributors must know to edit `wiki/` inside the code repo, not the wiki repo. Pointer page on the Gitea wiki explains.
|
||||
|
||||
## Implementation Notes
|
||||
- Required code/doc updates: `pumpingStation/wiki/{functional-description.md, README.md, modes/, diagrams/}` populated; `.wiki.git` Home shrunk.
|
||||
- Validation evidence required: Raw Gitea URLs resolve; `https://gitea.wbd-rd.nl/RnD/pumpingStation/src/branch/main/wiki/` browses cleanly.
|
||||
|
||||
## Rollback / Migration
|
||||
- Rollback strategy: Reverse — copy `pumpingStation/wiki/*.md` back into `.wiki.git`, update `.wiki.git` Home to point at itself.
|
||||
- Migration/deprecation plan: The pointer page stays indefinitely.
|
||||
@@ -1,36 +0,0 @@
|
||||
# DECISION-YYYYMMDD-<slug>
|
||||
|
||||
## Context
|
||||
- Task/request:
|
||||
- Impacted files/contracts:
|
||||
- Why a decision is required now:
|
||||
|
||||
## Options
|
||||
1. Option A
|
||||
- Benefits:
|
||||
- Risks:
|
||||
- Rollout notes:
|
||||
|
||||
2. Option B
|
||||
- Benefits:
|
||||
- Risks:
|
||||
- Rollout notes:
|
||||
|
||||
## Decision
|
||||
- Selected option:
|
||||
- Decision owner:
|
||||
- Date:
|
||||
- Rationale:
|
||||
|
||||
## Consequences
|
||||
- Compatibility impact:
|
||||
- Safety/security impact:
|
||||
- Data/operations impact:
|
||||
|
||||
## Implementation Notes
|
||||
- Required code/doc updates:
|
||||
- Validation evidence required:
|
||||
|
||||
## Rollback / Migration
|
||||
- Rollback strategy:
|
||||
- Migration/deprecation plan:
|
||||
@@ -1,15 +0,0 @@
|
||||
# EVOLV Decision Log
|
||||
|
||||
Use this folder to store high-impact agent/user decisions that affect compatibility, safety, security, schema, or rollout risk.
|
||||
|
||||
Naming:
|
||||
- `DECISION-YYYYMMDD-<slug>.md`
|
||||
|
||||
When to log:
|
||||
- topic/payload/API contract changes
|
||||
- safety envelope or fail-safe strategy changes
|
||||
- security posture/default changes
|
||||
- Influx retention/backfill/schema tradeoffs
|
||||
- explicit acceptance of deferred high-risk debt
|
||||
|
||||
Start from `DECISION_TEMPLATE.md` for new entries.
|
||||
@@ -41,13 +41,16 @@ You are the EVOLV orchestrator agent. You decompose complex tasks, route to spec
|
||||
- InfluxDB retention/backfill semantics or dashboard query contracts
|
||||
|
||||
## Reference Files
|
||||
- `CONTRACTS.md` (EVOLV root) — front-door map: where every contract, rule, and standard lives
|
||||
- `.claude/refactor/CONTRACTS.md` — platform API shapes (BaseDomain, BaseNodeAdapter, commands registry, …)
|
||||
- `.claude/refactor/OPEN_QUESTIONS.md` — live decisions log
|
||||
- `.agents/skills/evolv-orchestrator/SKILL.md` — Full orchestration protocol
|
||||
- `.agents/AGENTS.md` — Agent invocation policy, routing table, decision governance
|
||||
- `.agents/decisions/` — Decision log directory
|
||||
- `.agents/AGENTS.md` — Agent invocation policy and routing table
|
||||
- `.agents/improvements/IMPROVEMENTS_BACKLOG.md` — Deferred improvements
|
||||
|
||||
## Decision Governance
|
||||
- Record decision-gate outcomes in `.agents/decisions/DECISION-YYYYMMDD-<slug>.md`
|
||||
- Capture load-bearing decisions in the commit message and PR description
|
||||
- Live open questions belong in `.claude/refactor/OPEN_QUESTIONS.md`
|
||||
- Ask at most 3 questions per interview batch
|
||||
- Owner-approved defaults: compatibility=controlled, safety=availability-first
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# Continue here (fresh-context entry point)
|
||||
# Continue here (fresh-context entry point) — ARCHIVED
|
||||
|
||||
**Read this file first** if you're picking up the EVOLV refactor in a fresh
|
||||
session. It points at the durable plan and lists what's left.
|
||||
> [!WARNING]
|
||||
> **ARCHIVED — the refactor described here landed on `development` in May 2026.**
|
||||
> This file describes the *plan* and the *deferred work as of 2026-05-11*. It is
|
||||
> retained for history. For current work, start at [`../../../CONTRACTS.md`](../../../CONTRACTS.md)
|
||||
> (EVOLV root) and the live standards in [`../`](../).
|
||||
|
||||
Original intro: read this file first if you're picking up the EVOLV refactor
|
||||
in a fresh session. It points at the durable plan and lists what's left.
|
||||
|
||||
## Read in order
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
# Task list
|
||||
# Task list — ARCHIVED
|
||||
|
||||
> [!WARNING]
|
||||
> **ARCHIVED — Phases 1–11 landed on `development` in May 2026.**
|
||||
> This file is the original phased plan and is retained for history. For
|
||||
> deferred / open work, see [`../OPEN_QUESTIONS.md`](../OPEN_QUESTIONS.md).
|
||||
> For current standards, start at [`../../../CONTRACTS.md`](../../../CONTRACTS.md) (EVOLV root).
|
||||
|
||||
Phased and ordered. The TaskCreate tracker mirrors this list and is the
|
||||
active, mutable view; this file is the durable plan.
|
||||
@@ -1,82 +1,42 @@
|
||||
# EVOLV Platform Refactor — Guidelines
|
||||
# Platform Standards (post-refactor)
|
||||
|
||||
This directory holds the durable plan and conventions for the platform-wide
|
||||
refactor of the EVOLV Node-RED nodes. Anyone (human or agent) working on
|
||||
this refactor reads these files first.
|
||||
> **Front door:** start at [`CONTRACTS.md`](../../CONTRACTS.md) at the EVOLV root. It maps every contract, rule, and standard in the stack.
|
||||
|
||||
> **Fresh context? Start here:** read **[`CONTINUE_HERE.md`](./CONTINUE_HERE.md)** first.
|
||||
> It explains the current state (Phases 1–11 done, 823 platform tests green),
|
||||
> lists the deferred work in priority order, and points at the specific
|
||||
> `OPEN_QUESTIONS.md` entries that still need action.
|
||||
This directory holds the **live standards** that govern how every EVOLV node
|
||||
is shaped. They are the source of truth for any human or agent making a
|
||||
change. The platform refactor that produced them landed on `development` in
|
||||
May 2026; the plan artifacts that drove it are in [`Archive/`](./Archive/)
|
||||
for historical reference only.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Eliminate boilerplate** — every nodeClass today is ~80% identical.
|
||||
Move the shared parts into `generalFunctions/`. Each node keeps only
|
||||
what is genuinely node-specific.
|
||||
2. **Split big domain classes** — `pumpingStation`, `machineGroupControl`,
|
||||
and `rotatingMachine` each have ~1000–1800 line monolithic
|
||||
`specificClass.js` files mixing 6+ concerns. Split each into focused
|
||||
concern-based modules under `src/`.
|
||||
3. **Document the contract** — every msg.topic the node accepts and every
|
||||
message it emits is declared in code (a `commands/` module) and
|
||||
surfaced in a per-node `CONTRACT.md`.
|
||||
4. **Standardise naming** — consistent topic names across the platform
|
||||
(`set.<noun>`, `cmd.<verb>`, `evt.<noun>`).
|
||||
5. **Keep it readable** — small files, small functions, comments that say
|
||||
*why* and skip *what*.
|
||||
|
||||
## Constraint: this is the development branch
|
||||
|
||||
All 12 submodules + the parent EVOLV repo are on a `development` branch.
|
||||
`main` is untouched. We can change anything without breaking deployments
|
||||
that track `main`.
|
||||
|
||||
The refactor lands on `development`. Promotion to `main` happens once the
|
||||
whole platform passes its 3-tier tests + Docker E2E.
|
||||
|
||||
## Layered approach
|
||||
|
||||
The refactor is sequenced as **tiers**, not a big bang.
|
||||
|
||||
| Tier | What | Risk | Reversible? |
|
||||
|---|---|---|---|
|
||||
| 1 | Add infra in `generalFunctions` (additive only — no breaking changes) | Low | Yes |
|
||||
| 2 | Pilot one node (pumpingStation) end-to-end on the new infra | Med | Yes |
|
||||
| 3 | Convert remaining core nodes (measurement, MGC, rotatingMachine) | Med | Yes |
|
||||
| 4 | Convert remaining nodes (valve, VGC, reactor, settler, monster, diffuser, dashboardAPI) | Low | Yes |
|
||||
| 5 | Standardise input topic names + deprecation map | Med | Behind feature flag |
|
||||
| 6 | Promote `development` → `main` once Docker E2E green platform-wide | Low | Yes |
|
||||
| 7 | Wiki cleanup — visual-first template + Mermaid diagrams per node (post-refactor) | Low | Yes |
|
||||
|
||||
Each tier is a sequence of small PRs on `development`, each with its
|
||||
existing tests green.
|
||||
|
||||
## Files in this directory
|
||||
## Live standards (read these before changing code)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `README.md` | This file. |
|
||||
| `CONVENTIONS.md` | Code style, file size, comments, naming, imports, tests. |
|
||||
| `CONTRACTS.md` | The exact shapes — `BaseNodeAdapter`, `BaseDomain`, commands registry, child router, unit policy, status badge, output ports. |
|
||||
| `MODULE_SPLIT.md` | Per-node `src/` layout for the 4 core nodes + a generic template. |
|
||||
| `TASKS.md` | Phased task list. The `TaskCreate` task tree mirrors this and is the active tracker. |
|
||||
| `OPEN_QUESTIONS.md` | Decisions deferred to later — collected here so we don't lose them. |
|
||||
| [`CONTRACTS.md`](./CONTRACTS.md) | The exact API shapes — `BaseNodeAdapter`, `BaseDomain`, commands registry, `ChildRouter`, `UnitPolicy`, `statusBadge`, `HealthStatus`, `LatestWinsGate`, output ports, topic naming. |
|
||||
| [`CONVENTIONS.md`](./CONVENTIONS.md) | Code style, file/function size, comments, naming, imports, tests. |
|
||||
| [`MODULE_SPLIT.md`](./MODULE_SPLIT.md) | Per-node `src/` concern layout + the generic node template. |
|
||||
| [`WIKI_TEMPLATE.md`](./WIKI_TEMPLATE.md) | The 14-section visual-first template every per-node wiki uses. |
|
||||
| [`WIKI_HOME_TEMPLATE.md`](./WIKI_HOME_TEMPLATE.md) | The shape of each per-node `wiki/Home.md`. |
|
||||
| [`OPEN_QUESTIONS.md`](./OPEN_QUESTIONS.md) | Live decisions log — append-only. Most entries are resolved; unresolved entries are what's actually in play. |
|
||||
|
||||
## Workflow rules for spawned agents
|
||||
## How to use them
|
||||
|
||||
If you are an agent working on a refactor task:
|
||||
1. **Reading code in a node.** The node's `CONTRACT.md` and `src/commands/index.js`
|
||||
are the per-node contract; the files above are the platform contract those
|
||||
per-node files implement.
|
||||
2. **Writing new code in a node.** Match `MODULE_SPLIT.md` for layout, `CONVENTIONS.md`
|
||||
for style, `CONTRACTS.md` for the base-class API surface, and add to
|
||||
`OPEN_QUESTIONS.md` if you discover something unclear rather than inventing
|
||||
a decision.
|
||||
3. **Touching `generalFunctions`.** Any new export needs a `CONTRACT.md` entry
|
||||
in `nodes/generalFunctions/CONTRACT.md` and, if it introduces a new platform
|
||||
shape, a section in `CONTRACTS.md` here.
|
||||
4. **Updating a wiki page.** Generated sections (topic-contract, data-model)
|
||||
are produced by `npm run wiki:all` per submodule — never hand-edit between
|
||||
the `BEGIN AUTOGEN` / `END AUTOGEN` markers.
|
||||
|
||||
1. Read this file, `CONVENTIONS.md`, `CONTRACTS.md`, and the relevant
|
||||
section of `MODULE_SPLIT.md` before changing code.
|
||||
2. Stay within the scope of one task. Don't expand scope without flagging.
|
||||
3. Run the affected node's tests after every meaningful change. Commands:
|
||||
```
|
||||
cd nodes/<nodeName> && node --test test/basic test/integration test/edge
|
||||
```
|
||||
4. Don't change `generalFunctions` exports unless your task is in tier 1.
|
||||
5. If you discover something unclear, append it to `OPEN_QUESTIONS.md`
|
||||
with a short note. Do **not** invent a decision.
|
||||
6. Comments: small, function-level, *why* not *what*. See `CONVENTIONS.md`.
|
||||
7. When done, summarise: files changed, tests run, anything deferred to
|
||||
`OPEN_QUESTIONS.md`.
|
||||
## Archive
|
||||
|
||||
[`Archive/`](./Archive/) holds the refactor *plan* (now done): `CONTINUE_HERE.md`,
|
||||
`TASKS.md`. They describe how the platform got from pre-refactor to the
|
||||
current shape. They are **not** authoritative for new work — the files above are.
|
||||
|
||||
13
CLAUDE.md
13
CLAUDE.md
@@ -1,6 +1,8 @@
|
||||
# EVOLV - Claude Code Project Guide
|
||||
|
||||
> **READ FIRST, BEFORE ANY OTHER WORK:** `.claude/rules/repo-mem.md` — this repo has an MCP server (`repo-mem`) exposing a substrate-trained `repo_search` and a persistent fix-trace store. Use those instead of grep for concept queries, and record completed fixes via `repo_record_fix`. Triggers, anti-patterns, and refresh model are in that rule.
|
||||
> **READ FIRST, BEFORE ANY OTHER WORK:**
|
||||
> 1. [`CONTRACTS.md`](./CONTRACTS.md) — front-door map: where every contract, rule, and standard lives, and how to find them.
|
||||
> 2. [`.claude/rules/repo-mem.md`](./.claude/rules/repo-mem.md) — this repo has an MCP server (`repo-mem`) exposing a substrate-trained `repo_search` and a persistent fix-trace store. Use those instead of `grep` for concept queries, and record completed fixes via `repo_record_fix`.
|
||||
|
||||
## What This Is
|
||||
Node-RED custom nodes package for wastewater treatment plant automation. Developed by Waterschap Brabantse Delta R&D team. Follows ISA-88 (S88) batch control standard.
|
||||
@@ -29,6 +31,15 @@ Each node follows a three-layer pattern:
|
||||
- **Multi-tab demo flows**: see `.claude/rules/node-red-flow-layout.md` for the tab/link-channel/spacing rule set used by `examples/`
|
||||
- **Output coverage** (every output, every state, every layer): see `.claude/rules/output-coverage.md` — manifest + populated/degraded tests are mandatory for any change that touches Port 0/1/2 keys, function-node fan-outs, telemetry fields, or dashboard widget sources
|
||||
|
||||
## Sources of truth (the canonical files)
|
||||
- **Front-door map**: [`CONTRACTS.md`](./CONTRACTS.md) — read first; lists every standard and where it lives
|
||||
- **Platform API shapes** (BaseDomain, BaseNodeAdapter, commands registry, UnitPolicy, …): `.claude/refactor/CONTRACTS.md`
|
||||
- **Code conventions** (file/function size, comments, naming): `.claude/refactor/CONVENTIONS.md`
|
||||
- **Per-node module layout**: `.claude/refactor/MODULE_SPLIT.md`
|
||||
- **Per-node API contract**: `nodes/<n>/CONTRACT.md` + `nodes/<n>/src/commands/index.js` (source of truth for accepted `msg.topic` values)
|
||||
- **Shared library API**: `nodes/generalFunctions/CONTRACT.md` (exported classes + utilities)
|
||||
- **Live decisions log**: `.claude/refactor/OPEN_QUESTIONS.md` — append, don't invent
|
||||
|
||||
## Development Notes
|
||||
- No build step required - pure Node.js
|
||||
- Install: `npm install` in root
|
||||
|
||||
141
CONTRACTS.md
Normal file
141
CONTRACTS.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# EVOLV — Contracts, Rules, and Standards
|
||||
|
||||
> **Front door for humans and agents working in this repo.**
|
||||
> If you only read one file before touching code, read this one. It maps every
|
||||
> contract, rule, and standard in the EVOLV stack and tells you where each
|
||||
> lives. Everything else here is a link to a more specific file.
|
||||
|
||||
EVOLV is a Node-RED node library for wastewater treatment plant automation,
|
||||
built by Waterschap Brabantse Delta R&D. All work happens on the `development`
|
||||
branch across 12 submodules; promotion to `main` is gated by Docker E2E.
|
||||
|
||||
---
|
||||
|
||||
## 1. Where everything lives
|
||||
|
||||
### Platform-wide (EVOLV root)
|
||||
|
||||
| What | Where | Read when |
|
||||
|---|---|---|
|
||||
| **This map** | `CONTRACTS.md` (this file) | First time, or when orienting |
|
||||
| **Agent entry-point instructions** | [`CLAUDE.md`](./CLAUDE.md) | Auto-loaded by Claude Code |
|
||||
| **Active rules** | [`.claude/rules/`](./.claude/rules/) (7 files) | Triggered by `paths:` frontmatter or referenced from `CLAUDE.md` |
|
||||
| **Platform API contracts** | [`.claude/refactor/CONTRACTS.md`](./.claude/refactor/CONTRACTS.md) | Before changing `generalFunctions` exports or any base class |
|
||||
| **Code conventions** | [`.claude/refactor/CONVENTIONS.md`](./.claude/refactor/CONVENTIONS.md) | Before writing or editing any file |
|
||||
| **Per-node concern layout** | [`.claude/refactor/MODULE_SPLIT.md`](./.claude/refactor/MODULE_SPLIT.md) | When adding files to `nodes/<n>/src/` |
|
||||
| **Wiki page templates** | [`.claude/refactor/WIKI_TEMPLATE.md`](./.claude/refactor/WIKI_TEMPLATE.md) + [`WIKI_HOME_TEMPLATE.md`](./.claude/refactor/WIKI_HOME_TEMPLATE.md) | When editing a per-node wiki page |
|
||||
| **Live decisions log** | [`.claude/refactor/OPEN_QUESTIONS.md`](./.claude/refactor/OPEN_QUESTIONS.md) | When you spot an ambiguity — append, don't invent |
|
||||
| **Top-level wiki** | [`wiki/`](./wiki/) (Home, Architecture, Getting-Started, Telemetry, Topology-Patterns, Topic-Conventions, Glossary, Functional-Overview) | When you need a process-level or architecture-level view |
|
||||
| **Agent skills / routing** | [`.agents/AGENTS.md`](./.agents/AGENTS.md) + [`.agents/skills/`](./.agents/skills/) | When deciding which specialist to invoke |
|
||||
| **Improvements backlog** | [`.agents/improvements/IMPROVEMENTS_BACKLOG.md`](./.agents/improvements/IMPROVEMENTS_BACKLOG.md) | When deferring functional work |
|
||||
| **Repo-memory MCP rule** | [`.claude/rules/repo-mem.md`](./.claude/rules/repo-mem.md) | Before any concept-search — use `repo_search` instead of `grep` |
|
||||
|
||||
### Per-node (`nodes/<nodeName>/`)
|
||||
|
||||
| What | Where | Read when |
|
||||
|---|---|---|
|
||||
| Node entry instructions for agents | `nodes/<n>/CLAUDE.md` | Auto-loaded when touching files in that subtree |
|
||||
| **Node API contract** | `nodes/<n>/CONTRACT.md` | Before changing `msg.topic` inputs/outputs/events |
|
||||
| **Command registry (source of truth)** | `nodes/<n>/src/commands/index.js` | When adding/removing an accepted topic |
|
||||
| **Domain logic** | `nodes/<n>/src/specificClass.js` | Pure JS; no `RED.*` allowed |
|
||||
| **Node-RED adapter** | `nodes/<n>/src/nodeClass.js` | Bridge to runtime; ≤ 25 lines, extends `BaseNodeAdapter` |
|
||||
| **Per-node wiki** | `nodes/<n>/wiki/` — `Home.md`, `Reference-{Architecture,Contracts,Limitations,Examples}.md` | Topic-contract + data-model sections autogen via `npm run wiki:all` |
|
||||
| **Tests** | `nodes/<n>/test/{basic,integration,edge}/` | Required for every change |
|
||||
| **Example flows** | `nodes/<n>/examples/{basic,integration,edge}.flow.json` | Required artifact per node |
|
||||
|
||||
### Shared library (`nodes/generalFunctions/`)
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| **Library API contract** | `nodes/generalFunctions/CONTRACT.md` |
|
||||
| **Public exports** | `nodes/generalFunctions/index.js` (barrel) |
|
||||
| **Source** | `nodes/generalFunctions/src/{domain,nodered,measurements,convert,configs,…}/` |
|
||||
|
||||
### Archives (don't take as authoritative)
|
||||
|
||||
| What | Where | Why kept |
|
||||
|---|---|---|
|
||||
| Pre-refactor wiki pages | [`wiki/Archive/`](./wiki/Archive/) (20 files) | Historical reference; each has `⚠️ ARCHIVED — Do not update` |
|
||||
| Refactor plan artifacts | [`.claude/refactor/Archive/`](./.claude/refactor/Archive/) — `CONTINUE_HERE.md`, `TASKS.md` | The May-2026 refactor plan; phases all done |
|
||||
| Old priority lists | [`.agents/improvements/Archive/`](./.agents/improvements/Archive/) | Pre-refactor production priorities |
|
||||
|
||||
---
|
||||
|
||||
## 2. Discovery chain — how a fresh agent finds the rules
|
||||
|
||||
1. `CLAUDE.md` auto-loads → points at this file and `.claude/rules/repo-mem.md`.
|
||||
2. `.claude/rules/*.md` auto-load by `paths:` frontmatter when editing matching files.
|
||||
3. `nodes/<n>/CLAUDE.md` auto-loads when working under that submodule.
|
||||
4. This file (`CONTRACTS.md`) is the human-facing map of everything in step 1-3.
|
||||
5. **Concept lookup**: use the `repo-mem` MCP server (`repo_search`, `repo_similar_fixes`) — it's faster and more targeted than `grep`. See `.claude/rules/repo-mem.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. The three contracts every node honours
|
||||
|
||||
Every EVOLV node is a three-tier sandwich. Each tier has a contract:
|
||||
|
||||
| Tier | Class | Contract source | Per-node implementation |
|
||||
|---|---|---|---|
|
||||
| 1 — Entry | `RED.nodes.registerType` | [`.claude/rules/node-architecture.md`](./.claude/rules/node-architecture.md) | `nodes/<n>/<n>.js` |
|
||||
| 2 — Adapter | `BaseNodeAdapter` (from `generalFunctions`) | [`.claude/refactor/CONTRACTS.md §2`](./.claude/refactor/CONTRACTS.md) | `nodes/<n>/src/nodeClass.js` |
|
||||
| 3 — Domain | `BaseDomain` (from `generalFunctions`) | [`.claude/refactor/CONTRACTS.md §3`](./.claude/refactor/CONTRACTS.md) | `nodes/<n>/src/specificClass.js` |
|
||||
|
||||
Plus the **commands registry** (`nodes/<n>/src/commands/index.js`) declares
|
||||
the `msg.topic` inputs; `BaseNodeAdapter` dispatches by topic lookup. See
|
||||
[`.claude/refactor/CONTRACTS.md §4`](./.claude/refactor/CONTRACTS.md).
|
||||
|
||||
---
|
||||
|
||||
## 4. Output and telemetry contract
|
||||
|
||||
Three output ports per node. Source of truth: [`.claude/refactor/CONTRACTS.md §10`](./.claude/refactor/CONTRACTS.md) and [`wiki/Telemetry.md`](./wiki/Telemetry.md).
|
||||
|
||||
| Port | Carries | Formatter |
|
||||
|---|---|---|
|
||||
| 0 | Process data (delta-compressed) | `outputUtils.formatMsg(..., 'process')` |
|
||||
| 1 | InfluxDB line protocol | `outputUtils.formatMsg(..., 'influxdb')` |
|
||||
| 2 | Registration / control plumbing | hand-shaped on the parent-child handshake |
|
||||
|
||||
Output-coverage testing (manifest + populated + degraded states) is **mandatory**
|
||||
for any change touching Port 0/1/2 keys, function-node fan-outs, or dashboard widgets.
|
||||
See [`.claude/rules/output-coverage.md`](./.claude/rules/output-coverage.md).
|
||||
|
||||
---
|
||||
|
||||
## 5. When a contract changes — the rule
|
||||
|
||||
1. Update the source file (`src/commands/index.js`, `src/specificClass.js`, or `generalFunctions/index.js`).
|
||||
2. Update the per-node `CONTRACT.md` (Inputs table is partially autogenerated; the rest is hand-maintained).
|
||||
3. Run `npm run wiki:all` inside the submodule to regenerate the topic-contract + data-model sections in `wiki/`.
|
||||
4. If the change touched a platform shape (a base class or shared utility), update [`.claude/refactor/CONTRACTS.md`](./.claude/refactor/CONTRACTS.md) and `nodes/generalFunctions/CONTRACT.md`.
|
||||
5. If the change introduced a deprecation, add an alias to `commands/index.js` and a one-line note to the per-node `CONTRACT.md`.
|
||||
6. Append unresolved questions to [`.claude/refactor/OPEN_QUESTIONS.md`](./.claude/refactor/OPEN_QUESTIONS.md). Don't invent answers.
|
||||
|
||||
---
|
||||
|
||||
## 6. Conventions in one paragraph (the rest is in `CONVENTIONS.md`)
|
||||
|
||||
Files ≤ 200 lines (300 hard cap); functions ≤ 30 lines (60 hard cap). Default
|
||||
to no comments — add one only when *why* is non-obvious. `specificClass`
|
||||
**never** imports `RED.*`. Logger from `generalFunctions`, never `console.log`.
|
||||
S88 colour scheme is mandatory in diagrams. Topic prefixes: `set.<noun>` for
|
||||
idempotent setters, `cmd.<verb>` for triggers, `evt.<noun>` for events.
|
||||
Tests live in `test/{basic,integration,edge}/`. Submodule commits go in the
|
||||
submodule first, then the superproject bumps the pin.
|
||||
|
||||
---
|
||||
|
||||
## 7. Verification checklist before merge
|
||||
|
||||
- [ ] Per-node tests green (`cd nodes/<n> && node --test test/basic test/integration test/edge`).
|
||||
- [ ] `CONTRACT.md` updated for any added / removed / renamed topic, port-0 key, or event.
|
||||
- [ ] `npm run wiki:all` re-run in the touched submodule(s).
|
||||
- [ ] Output-coverage manifest + tests updated if any output shape changed.
|
||||
- [ ] Submodule pin bumped in the superproject.
|
||||
- [ ] Commit message captures *why* — load-bearing decisions go in the commit body and PR description.
|
||||
|
||||
---
|
||||
|
||||
*Last reviewed: 2026-05-18. If something in this map is wrong, fix this file
|
||||
in the same PR as the change that made it wrong.*
|
||||
@@ -1,602 +0,0 @@
|
||||
# EVOLV Developer Guide: Creating a New Node
|
||||
|
||||
This guide walks through creating a new EVOLV node from scratch, following the project's three-layer architecture pattern.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** (v18+)
|
||||
- **Node-RED** installed globally or as a dev dependency
|
||||
- Clone the repo and run `npm install` in the root (no build step required)
|
||||
- The `generalFunctions` submodule must be initialized (`git submodule update --init`)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Every EVOLV node follows a **three-layer pattern**:
|
||||
|
||||
| Layer | File | Responsibility |
|
||||
|-------|------|---------------|
|
||||
| 1 - Wrapper | `<name>.js` | Registers the node type with Node-RED, sets up HTTP endpoints for menus/config |
|
||||
| 2 - Node Adapter | `src/nodeClass.js` | Bridges Node-RED with domain logic: config loading, tick loop, input routing, lifecycle |
|
||||
| 3 - Domain Logic | `src/specificClass.js` | Pure business logic with no Node-RED dependencies |
|
||||
|
||||
Plus a UI definition: `<name>.html` for the Node-RED editor.
|
||||
|
||||
## Step-by-Step: Creating a New Node
|
||||
|
||||
We will create a hypothetical `flowMeter` node as an example.
|
||||
|
||||
### Step 1: Create Directory Structure
|
||||
|
||||
```
|
||||
nodes/flowMeter/
|
||||
flowMeter.js # Layer 1 - wrapper
|
||||
flowMeter.html # UI definition
|
||||
src/
|
||||
nodeClass.js # Layer 2 - node adapter
|
||||
specificClass.js # Layer 3 - domain logic
|
||||
test/
|
||||
specificClass.test.js
|
||||
```
|
||||
|
||||
### Step 2: Write the Wrapper (`flowMeter.js`)
|
||||
|
||||
The wrapper registers the node type with Node-RED and exposes HTTP endpoints for dynamic menus and config data.
|
||||
|
||||
```js
|
||||
const nameOfNode = 'flowMeter';
|
||||
const nodeClass = require('./src/nodeClass.js');
|
||||
const { MenuManager, configManager } = require('generalFunctions');
|
||||
|
||||
module.exports = function(RED) {
|
||||
// Register the node type
|
||||
RED.nodes.registerType(nameOfNode, function(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
|
||||
});
|
||||
|
||||
// Menu endpoint (dynamic dropdowns in the editor UI)
|
||||
const menuMgr = new MenuManager();
|
||||
RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
|
||||
try {
|
||||
const script = menuMgr.createEndpoint(nameOfNode, ['asset', 'logger', 'position']);
|
||||
res.type('application/javascript').send(script);
|
||||
} catch (err) {
|
||||
res.status(500).send(`// Error generating menu: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Config data endpoint (exposes JSON config to the editor)
|
||||
const cfgMgr = new configManager();
|
||||
RED.httpAdmin.get(`/${nameOfNode}/configData.js`, (req, res) => {
|
||||
try {
|
||||
const script = cfgMgr.createEndpoint(nameOfNode);
|
||||
res.type('application/javascript').send(script);
|
||||
} catch (err) {
|
||||
res.status(500).send(`// Error generating configData: ${err.message}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `nameOfNode` must match the file name, the `registerType` name, and the HTML `data-template-name`.
|
||||
- Menu categories (`['asset', 'logger', 'position']`) control which shared UI sections appear.
|
||||
- The config endpoint is optional if you do not need dynamic config in the editor.
|
||||
|
||||
### Step 3: Write `nodeClass.js` (Node Adapter)
|
||||
|
||||
This class bridges Node-RED's API with your domain logic.
|
||||
|
||||
```js
|
||||
const { outputUtils, configManager } = require('generalFunctions');
|
||||
const Specific = require('./specificClass');
|
||||
|
||||
class nodeClass {
|
||||
constructor(uiConfig, RED, nodeInstance, nameOfNode) {
|
||||
this.node = nodeInstance;
|
||||
this.RED = RED;
|
||||
this.name = nameOfNode;
|
||||
|
||||
this._loadConfig(uiConfig);
|
||||
this._setupSpecificClass();
|
||||
this._bindEvents();
|
||||
this._registerChild();
|
||||
this._startTickLoop();
|
||||
this._attachInputHandler();
|
||||
this._attachCloseHandler();
|
||||
}
|
||||
|
||||
_loadConfig(uiConfig) {
|
||||
const cfgMgr = new configManager();
|
||||
this.defaultConfig = cfgMgr.getConfig(this.name);
|
||||
|
||||
// buildConfig merges base sections (general, asset, functionality)
|
||||
// with node-specific domain config from the UI
|
||||
this.config = cfgMgr.buildConfig(this.name, uiConfig, this.node.id, {
|
||||
// Add domain-specific config sections here:
|
||||
flowSettings: {
|
||||
maxFlow: uiConfig.maxFlow,
|
||||
pipeSize: uiConfig.pipeSize,
|
||||
},
|
||||
});
|
||||
|
||||
this._output = new outputUtils();
|
||||
}
|
||||
|
||||
_setupSpecificClass() {
|
||||
this.source = new Specific(this.config);
|
||||
this.node.source = this.source;
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
// Subscribe to domain events for Node-RED status display
|
||||
this.source.emitter.on('flowUpdate', (val) => {
|
||||
this.node.status({ fill: 'green', shape: 'dot', text: `${val} ${this.config.general.unit}` });
|
||||
});
|
||||
}
|
||||
|
||||
_registerChild() {
|
||||
// Delayed to avoid Node-RED startup race conditions
|
||||
setTimeout(() => {
|
||||
this.node.send([
|
||||
null,
|
||||
null,
|
||||
{
|
||||
topic: 'registerChild',
|
||||
payload: this.node.id,
|
||||
positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment',
|
||||
distance: this.config?.functionality?.distance || null,
|
||||
},
|
||||
]);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_startTickLoop() {
|
||||
setTimeout(() => {
|
||||
this._tickInterval = setInterval(() => this._tick(), 1000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_tick() {
|
||||
this.source.tick();
|
||||
const raw = this.source.getOutput();
|
||||
const processMsg = this._output.formatMsg(raw, this.config, 'process');
|
||||
const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb');
|
||||
this.node.send([processMsg, influxMsg]);
|
||||
}
|
||||
|
||||
_attachInputHandler() {
|
||||
this.node.on('input', (msg, send, done) => {
|
||||
switch (msg.topic) {
|
||||
case 'measurement':
|
||||
if (typeof msg.payload === 'number') {
|
||||
this.source.inputValue = parseFloat(msg.payload);
|
||||
}
|
||||
break;
|
||||
// Add more input topics as needed
|
||||
}
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
_attachCloseHandler() {
|
||||
this.node.on('close', (done) => {
|
||||
clearInterval(this._tickInterval);
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nodeClass;
|
||||
```
|
||||
|
||||
Essential methods every `nodeClass` must implement:
|
||||
- `_loadConfig()` -- merges default JSON config with UI config via `configManager.buildConfig()`
|
||||
- `_setupSpecificClass()` -- instantiates the domain class
|
||||
- `_registerChild()` -- sends a `registerChild` message on output port 2 (parent)
|
||||
- `_startTickLoop()` -- drives periodic output at 1-second intervals
|
||||
- `_tick()` -- calls `source.getOutput()` and formats via `outputUtils.formatMsg()`
|
||||
- `_attachInputHandler()` -- routes incoming `msg.topic` to domain methods
|
||||
- `_attachCloseHandler()` -- clears timers on node removal
|
||||
|
||||
### Step 4: Write `specificClass.js` (Domain Logic)
|
||||
|
||||
This is pure JavaScript with no Node-RED dependencies.
|
||||
|
||||
```js
|
||||
const EventEmitter = require('events');
|
||||
const { logger, configUtils, configManager, MeasurementContainer } = require('generalFunctions');
|
||||
|
||||
class FlowMeter {
|
||||
constructor(config = {}) {
|
||||
this.emitter = new EventEmitter();
|
||||
this.configManager = new configManager();
|
||||
this.defaultConfig = this.configManager.getConfig('flowMeter');
|
||||
this.configUtils = new configUtils(this.defaultConfig);
|
||||
this.config = this.configUtils.initConfig(config);
|
||||
|
||||
this.logger = new logger(
|
||||
this.config.general.logging.enabled,
|
||||
this.config.general.logging.logLevel,
|
||||
this.config.general.name
|
||||
);
|
||||
|
||||
// MeasurementContainer stores typed/positioned values
|
||||
this.measurements = new MeasurementContainer({
|
||||
autoConvert: true,
|
||||
windowSize: this.config.smoothing?.smoothWindow || 10,
|
||||
});
|
||||
this.measurements.setChildId(this.config.general.id);
|
||||
this.measurements.setChildName(this.config.general.name);
|
||||
|
||||
// Domain state
|
||||
this.currentFlow = 0;
|
||||
}
|
||||
|
||||
tick() {
|
||||
// Called every 1 second by nodeClass._tick()
|
||||
this.calculateFlow();
|
||||
}
|
||||
|
||||
calculateFlow() {
|
||||
// Your domain logic here
|
||||
const flow = this.currentFlow;
|
||||
|
||||
// Store in MeasurementContainer using the chainable API:
|
||||
// .type(measType).variant(variant).position(pos).value(val, timestamp, unit)
|
||||
this.measurements
|
||||
.type(this.config.asset.type)
|
||||
.variant('measured')
|
||||
.position(this.config.functionality.positionVsParent)
|
||||
.value(flow, Date.now(), this.config.asset.unit);
|
||||
|
||||
this.emitter.emit('flowUpdate', flow);
|
||||
}
|
||||
|
||||
getOutput() {
|
||||
return {
|
||||
flow: this.currentFlow,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FlowMeter;
|
||||
```
|
||||
|
||||
Key patterns:
|
||||
- Always create an `emitter` (EventEmitter) -- parents subscribe to child events through it.
|
||||
- Use `MeasurementContainer` for storing measurements. The chainable API follows the pattern: `measurements.type(t).variant(v).position(p).value(val, timestamp, unit)`.
|
||||
- Expose `tick()` and `getOutput()` for the node adapter to call.
|
||||
- Use `logger` instead of `console.log`.
|
||||
|
||||
### Step 5: Write the HTML (UI Definition)
|
||||
|
||||
```html
|
||||
<script src="/flowMeter/menu.js"></script>
|
||||
<script src="/flowMeter/configData.js"></script>
|
||||
|
||||
<script>
|
||||
RED.nodes.registerType("flowMeter", {
|
||||
category: "EVOLV",
|
||||
color: "#a9daee", // S88 Control Module color
|
||||
defaults: {
|
||||
name: { value: "flowMeter" },
|
||||
maxFlow: { value: 100, required: true },
|
||||
pipeSize: { value: 0.1, required: true },
|
||||
// Standard fields (asset, logger, position)
|
||||
uuid: { value: "" },
|
||||
supplier: { value: "" },
|
||||
category: { value: "" },
|
||||
assetType: { value: "" },
|
||||
model: { value: "" },
|
||||
unit: { value: "" },
|
||||
enableLog: { value: false },
|
||||
logLevel: { value: "error" },
|
||||
positionVsParent: { value: "" },
|
||||
positionIcon: { value: "" },
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 3,
|
||||
outputLabels: ["process", "dbase", "parent"],
|
||||
icon: "font-awesome/fa-tachometer",
|
||||
label: function() {
|
||||
return this.name || "flowMeter";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
// Wait for shared menu system to initialize
|
||||
const waitForMenuData = () => {
|
||||
if (window.EVOLV?.nodes?.flowMeter?.initEditor) {
|
||||
window.EVOLV.nodes.flowMeter.initEditor(this);
|
||||
} else {
|
||||
setTimeout(waitForMenuData, 50);
|
||||
}
|
||||
};
|
||||
waitForMenuData();
|
||||
},
|
||||
oneditsave: function() {
|
||||
if (window.EVOLV?.nodes?.flowMeter?.assetMenu?.saveEditor) {
|
||||
window.EVOLV.nodes.flowMeter.assetMenu.saveEditor(this);
|
||||
}
|
||||
if (window.EVOLV?.nodes?.flowMeter?.loggerMenu?.saveEditor) {
|
||||
window.EVOLV.nodes.flowMeter.loggerMenu.saveEditor(this);
|
||||
}
|
||||
if (window.EVOLV?.nodes?.flowMeter?.positionMenu?.saveEditor) {
|
||||
window.EVOLV.nodes.flowMeter.positionMenu.saveEditor(this);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="flowMeter">
|
||||
<div class="form-row">
|
||||
<label for="node-input-maxFlow"><i class="fa fa-arrows-v"></i> Max Flow</label>
|
||||
<input type="number" id="node-input-maxFlow" placeholder="100" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-pipeSize"><i class="fa fa-circle-o"></i> Pipe Size (m)</label>
|
||||
<input type="number" id="node-input-pipeSize" placeholder="0.1" step="0.01" />
|
||||
</div>
|
||||
<!-- Shared UI sections injected by MenuManager -->
|
||||
<div id="asset-fields-placeholder"></div>
|
||||
<div id="logger-fields-placeholder"></div>
|
||||
<div id="position-fields-placeholder"></div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="flowMeter">
|
||||
<p><b>Flow Meter Node</b>: Measures and processes flow data.</p>
|
||||
</script>
|
||||
```
|
||||
|
||||
**S88 color scheme** (pick based on your node's hierarchy level):
|
||||
|
||||
| S88 Level | Color | Text Color |
|
||||
|-----------|-------|-----------|
|
||||
| Area | `#0f52a5` | white |
|
||||
| Process Cell | `#0c99d9` | white |
|
||||
| Unit | `#50a8d9` | black |
|
||||
| Equipment | `#86bbdd` | black |
|
||||
| Control Module | `#a9daee` | black |
|
||||
|
||||
All nodes must have **3 outputs**: `[process, dbase, parent]`.
|
||||
|
||||
### Step 6: Create Config JSON Schema
|
||||
|
||||
Create `nodes/generalFunctions/src/configs/flowMeter.json`. This defines defaults and validation rules for every config property. The `configManager` reads this file by node name.
|
||||
|
||||
```json
|
||||
{
|
||||
"general": {
|
||||
"name": {
|
||||
"default": "FlowMeter",
|
||||
"rules": { "type": "string", "description": "Human-readable name." }
|
||||
},
|
||||
"id": {
|
||||
"default": null,
|
||||
"rules": { "type": "string", "nullable": true }
|
||||
},
|
||||
"unit": {
|
||||
"default": "m3/h",
|
||||
"rules": { "type": "string" }
|
||||
},
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"default": "info",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{ "value": "debug" }, { "value": "info" },
|
||||
{ "value": "warn" }, { "value": "error" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"enabled": { "default": true, "rules": { "type": "boolean" } }
|
||||
}
|
||||
},
|
||||
"functionality": {
|
||||
"softwareType": { "default": "flowMeter", "rules": { "type": "string" } },
|
||||
"role": { "default": "Sensor", "rules": { "type": "string" } },
|
||||
"positionVsParent": {
|
||||
"default": "atEquipment",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{ "value": "atEquipment" }, { "value": "upstream" }, { "value": "downstream" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"asset": {
|
||||
"supplier": { "default": "Unknown", "rules": { "type": "string" } },
|
||||
"category": { "default": "sensor", "rules": { "type": "string" } },
|
||||
"type": { "default": "flow", "rules": { "type": "string" } },
|
||||
"model": { "default": "Unknown", "rules": { "type": "string" } },
|
||||
"unit": { "default": "m3/h", "rules": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each property has a `default` value and a `rules` object specifying the type (`string`, `number`, `boolean`, `enum`, `object`), optional constraints (`min`, `max`, `nullable`), and a description.
|
||||
|
||||
### Step 7: Register with `package.json`
|
||||
|
||||
Add your node to the root `package.json` under `node-red.nodes`:
|
||||
|
||||
```json
|
||||
{
|
||||
"node-red": {
|
||||
"nodes": {
|
||||
"flowMeter": "nodes/flowMeter/flowMeter.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Node-RED to pick up the new node.
|
||||
|
||||
### Step 8: Add Tests
|
||||
|
||||
Create `nodes/flowMeter/test/specificClass.test.js`. Tests target Layer 3 (domain logic) directly, without Node-RED.
|
||||
|
||||
```js
|
||||
const FlowMeter = require('../src/specificClass');
|
||||
|
||||
function makeConfig(overrides = {}) {
|
||||
const base = {
|
||||
general: { name: 'TestFlow', id: 'test-1', logging: { enabled: false, logLevel: 'error' } },
|
||||
functionality: { softwareType: 'flowMeter', role: 'sensor', positionVsParent: 'atEquipment' },
|
||||
asset: { category: 'sensor', type: 'flow', model: 'test', supplier: 'Test', unit: 'm3/h' },
|
||||
};
|
||||
for (const key of Object.keys(overrides)) {
|
||||
base[key] = typeof overrides[key] === 'object' ? { ...base[key], ...overrides[key] } : overrides[key];
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
describe('FlowMeter specificClass', () => {
|
||||
it('should create an instance', () => {
|
||||
const fm = new FlowMeter(makeConfig());
|
||||
expect(fm).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return output with expected keys', () => {
|
||||
const fm = new FlowMeter(makeConfig());
|
||||
const out = fm.getOutput();
|
||||
expect(out).toHaveProperty('flow');
|
||||
});
|
||||
|
||||
it('tick() should not throw', () => {
|
||||
const fm = new FlowMeter(makeConfig());
|
||||
expect(() => fm.tick()).not.toThrow();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run tests with: `npm test` (uses Jest). The project also supports `node:test` for basic smoke tests.
|
||||
|
||||
**Test organization conventions** (based on existing nodes):
|
||||
- `test/specificClass.test.js` -- unit tests for domain logic
|
||||
- `test/basic/*.test.js` -- structural/smoke tests (module loads, exports exist)
|
||||
- `test/edge/*.test.js` -- edge case and boundary tests
|
||||
- `test/integration/*.test.js` -- multi-component integration tests
|
||||
|
||||
## Key APIs Reference
|
||||
|
||||
### MeasurementContainer
|
||||
|
||||
Chainable storage for typed, positioned measurements. Used by every domain class.
|
||||
|
||||
```js
|
||||
const { MeasurementContainer } = require('generalFunctions');
|
||||
const mc = new MeasurementContainer({ autoConvert: true, windowSize: 10 });
|
||||
mc.setChildId('node-id');
|
||||
mc.setChildName('PT-001');
|
||||
|
||||
// Store a value
|
||||
mc.type('pressure').variant('measured').position('upstream').value(3.5, Date.now(), 'bar');
|
||||
|
||||
// Parents subscribe to events via mc.emitter
|
||||
mc.emitter.on('pressure.measured.upstream', (data) => { /* { value, unit, ... } */ });
|
||||
```
|
||||
|
||||
The event name follows the pattern: `{type}.{variant}.{position}`.
|
||||
|
||||
### configManager.buildConfig()
|
||||
|
||||
Merges the JSON config schema defaults with UI-provided values. Called in `nodeClass._loadConfig()`.
|
||||
|
||||
```js
|
||||
const { configManager } = require('generalFunctions');
|
||||
const cfgMgr = new configManager();
|
||||
const defaults = cfgMgr.getConfig('myNode'); // loads myNode.json
|
||||
const config = cfgMgr.buildConfig('myNode', uiConfig, nodeId, domainOverrides);
|
||||
```
|
||||
|
||||
### POSITIONS
|
||||
|
||||
Canonical position constants. Use these instead of hardcoded strings.
|
||||
|
||||
```js
|
||||
const { POSITIONS } = require('generalFunctions');
|
||||
// POSITIONS.UPSTREAM = 'upstream'
|
||||
// POSITIONS.DOWNSTREAM = 'downstream'
|
||||
// POSITIONS.AT_EQUIPMENT = 'atEquipment'
|
||||
// POSITIONS.DELTA = 'delta'
|
||||
```
|
||||
|
||||
### outputUtils.formatMsg()
|
||||
|
||||
Formats raw output data into either `process` or `influxdb` messages. Only sends changed fields.
|
||||
|
||||
```js
|
||||
const { outputUtils } = require('generalFunctions');
|
||||
const out = new outputUtils();
|
||||
const processMsg = out.formatMsg(rawData, config, 'process');
|
||||
const influxMsg = out.formatMsg(rawData, config, 'influxdb');
|
||||
node.send([processMsg, influxMsg]);
|
||||
```
|
||||
|
||||
### childRegistrationUtils
|
||||
|
||||
Manages parent-child node relationships. Parents use this to accept child registrations.
|
||||
|
||||
```js
|
||||
const { childRegistrationUtils } = require('generalFunctions');
|
||||
const regUtils = new childRegistrationUtils(this); // 'this' is the parent specificClass
|
||||
// Called when a child's registerChild message arrives:
|
||||
regUtils.registerChild(childSource, positionVsParent, distance);
|
||||
```
|
||||
|
||||
The parent's `registerChild()` method subscribes to the child's `measurements.emitter` events for data propagation.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Parent-Child Registration
|
||||
|
||||
1. Child sends `{ topic: 'registerChild', payload: nodeId, positionVsParent }` on output port 2.
|
||||
2. Parent's `nodeClass._attachInputHandler()` catches `msg.topic === 'registerChild'`.
|
||||
3. Parent calls `childRegistrationUtils.registerChild(child, position)`.
|
||||
4. Parent subscribes to child's `measurements.emitter` events (e.g., `'flow.measured.downstream'`).
|
||||
5. When the child updates a measurement, the parent's listener fires and updates its own state.
|
||||
|
||||
### Tick Loop
|
||||
|
||||
Every node runs a 1-second tick loop that:
|
||||
1. Calls `source.tick()` to advance domain logic.
|
||||
2. Calls `source.getOutput()` for current state.
|
||||
3. Formats into `process` and `influxdb` messages via `outputUtils.formatMsg()`.
|
||||
4. Sends on ports 0 (process) and 1 (dbase).
|
||||
|
||||
The tick loop starts with a 1-second delay to allow child registration to complete.
|
||||
|
||||
### Three-Output Format
|
||||
|
||||
All nodes send on three ports: `node.send([processMsg, influxMsg, parentMsg])`.
|
||||
|
||||
| Port | Purpose | When |
|
||||
|------|---------|------|
|
||||
| 0 | Process data for downstream nodes | Every tick (if changed) |
|
||||
| 1 | InfluxDB line protocol for persistence | Every tick (if changed) |
|
||||
| 2 | Parent registration/control messages | On startup; on parent commands |
|
||||
|
||||
### Event-Driven Communication
|
||||
|
||||
Nodes communicate via `EventEmitter`, not Node-RED wires:
|
||||
- `measurements.emitter` fires `{type}.{variant}.{position}` events.
|
||||
- Parents listen to children's emitters after registration.
|
||||
- The `emitter` on the specificClass itself is used for internal state changes (e.g., updating Node-RED node status display).
|
||||
|
||||
## Checklist
|
||||
|
||||
Before submitting a new node, verify:
|
||||
|
||||
- [ ] Three-layer structure: wrapper, nodeClass, specificClass
|
||||
- [ ] Config JSON in `generalFunctions/src/configs/<name>.json`
|
||||
- [ ] Registered in root `package.json` under `node-red.nodes`
|
||||
- [ ] HTML registers under category `'EVOLV'` with correct S88 color
|
||||
- [ ] Three outputs: `[process, dbase, parent]`
|
||||
- [ ] Uses `logger` (not `console.log`)
|
||||
- [ ] Uses `MeasurementContainer` for measurement storage
|
||||
- [ ] Uses `outputUtils.formatMsg()` for output formatting
|
||||
- [ ] Tick loop cleans up in `_attachCloseHandler()`
|
||||
- [ ] Tests exist for specificClass domain logic
|
||||
- [ ] Node-specific UI fields plus shared placeholders (asset, logger, position)
|
||||
Submodule nodes/generalFunctions updated: 34a4ef0610...49c77f262f
Reference in New Issue
Block a user