Compare commits

...

3 Commits

Author SHA1 Message Date
znetsixe
7ded2a4415 docs: consolidate scattered documentation into wiki
Some checks failed
CI / lint-and-test (push) Has been cancelled
Move architecture/, docs/ content into wiki/ for a single source of truth:
- architecture/deployment-blueprint.md → wiki/architecture/
- architecture/stack-architecture-review.md → wiki/architecture/
- architecture/wiki-platform-overview.md → wiki/architecture/
- docs/ARCHITECTURE.md → wiki/architecture/node-architecture.md
- docs/API_REFERENCE.md → wiki/concepts/generalfunctions-api.md
- docs/ISSUES.md → wiki/findings/open-issues-2026-03.md

Remove stale files:
- FUNCTIONAL_ISSUES_BACKLOG.md (was just a redirect pointer)
- temp/ (stale cloud env examples)

Fix README.md gitea URL (centraal.wbd-rd.nl → wbd-rd.nl).
Update wiki index with all consolidated pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:08:35 +02:00
znetsixe
6d19038784 docs: initialize project wiki from production hardening session
12 pages covering architecture, findings, and metrics from the
rotatingMachine + machineGroupControl hardening work:

- Overview: node inventory, what works/doesn't, current scale
- Architecture: 3D pump curves, group optimization algorithm
- Findings: BEP-Gravitation proof (0.1% of optimum), NCog behavior,
  curve non-convexity, pump switching stability
- Metrics: test counts, power comparison table, performance numbers
- Knowledge graph: structured YAML with all data points and provenance
- Session log: 2026-04-07 production hardening
- Tools: query.py, search.sh, lint.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:36:08 +02:00
znetsixe
fd9d1679cb fix: update submodule refs — production hardening for rotatingMachine and machineGroupControl
rotatingMachine:
- Safety fixes: async input handler, emergencyStop case fix, null guards,
  listener cleanup, tick loop race condition, editor timeout
- Prediction: remove efficiency rounding (was breaking NCog/BEP), fix
  variant reads, curve anomaly detection
- 43 new tests (76 total)

machineGroupControl:
- Critical: fix flowmovement unit mismatch (m³/s sent where m³/h expected,
  pumps never moved from minimum)
- Fix absolute scaling comparison bug, empty Qd block, empty-machines guards
- Add marginal-cost refinement loop: reduces gap to brute-force optimum
  from 2.1% to <0.1%
- 2 new test files with NCog distribution and power comparison tests

generalFunctions:
- Fix 3 anomalous power values in hidrostal-H05K-S03R curve data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:41:22 +02:00
29 changed files with 1096 additions and 152 deletions

View File

@@ -1,6 +0,0 @@
# Functional Issues Backlog (Deprecated Location)
This backlog has moved to:
- `.agents/improvements/IMPROVEMENTS_BACKLOG.md`
Use `.agents/improvements/TOP10_PRODUCTION_PRIORITIES_YYYY-MM-DD.md` for ranked review lists.

View File

@@ -40,7 +40,7 @@ Alle bouwblokken van het R&D-team zijn gebundeld in de **EVOLV-repository**, waa
### Eerste keer klonen: ### Eerste keer klonen:
```bash ```bash
git clone --recurse-submodules https://gitea.centraal.wbd-rd.nl/RnD/EVOLV.git git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git
cd EVOLV cd EVOLV
``` ```
@@ -77,7 +77,7 @@ git commit -m "Update submodule <bouwblok-naam>"
1. Clone de gewenste repository: 1. Clone de gewenste repository:
```bash ```bash
git clone https://gitea.centraal.wbd-rd.nl/<repo-naam>.git git clone https://gitea.wbd-rd.nl/<repo-naam>.git
``` ```
2. Kopieer het bouwblok naar je Node-RED map: 2. Kopieer het bouwblok naar je Node-RED map:

View File

@@ -1,24 +0,0 @@
# Copy this file to `.env` on the target server and populate real values there.
# Keep the real `.env` out of version control.
INFLUXDB_ADMIN_USER=replace-me
INFLUXDB_ADMIN_PASSWORD=replace-me
INFLUXDB_BUCKET=lvl0
INFLUXDB_ORG=wbd
GF_SECURITY_ADMIN_USER=replace-me
GF_SECURITY_ADMIN_PASSWORD=replace-me
NPM_DB_MYSQL_HOST=db
NPM_DB_MYSQL_PORT=3306
NPM_DB_MYSQL_USER=npm
NPM_DB_MYSQL_PASSWORD=replace-me
NPM_DB_MYSQL_NAME=npm
MYSQL_ROOT_PASSWORD=replace-me
MYSQL_DATABASE=npm
MYSQL_USER=npm
MYSQL_PASSWORD=replace-me
RABBITMQ_DEFAULT_USER=replace-me
RABBITMQ_DEFAULT_PASS=replace-me

View File

@@ -1,117 +0,0 @@
services:
node-red:
image: nodered/node-red:latest
container_name: node-red
restart: always
ports:
- "1880:1880"
volumes:
- node_red_data:/data
influxdb:
image: influxdb:2.7
container_name: influxdb
restart: always
ports:
- "8086:8086"
environment:
- INFLUXDB_ADMIN_USER=${INFLUXDB_ADMIN_USER}
- INFLUXDB_ADMIN_PASSWORD=${INFLUXDB_ADMIN_PASSWORD}
- INFLUXDB_BUCKET=${INFLUXDB_BUCKET}
- INFLUXDB_ORG=${INFLUXDB_ORG}
volumes:
- influxdb_data:/var/lib/influxdb2
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: always
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER}
- GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}
volumes:
- grafana_data:/var/lib/grafana
depends_on:
- influxdb
jenkins:
image: jenkins/jenkins:lts
container_name: jenkins
restart: always
ports:
- "8080:8080" # Web
- "50000:50000" # Agents
volumes:
- jenkins_home:/var/jenkins_home
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: always
environment:
- USER_UID=1000
- USER_GID=1000
ports:
- "3001:3000" # Webinterface (anders dan Grafana)
- "222:22" # SSH voor Git
volumes:
- gitea_data:/data
proxymanager:
image: jc21/nginx-proxy-manager:latest
container_name: proxymanager
restart: always
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "81:81" # Admin UI
environment:
DB_MYSQL_HOST: ${NPM_DB_MYSQL_HOST:-db}
DB_MYSQL_PORT: ${NPM_DB_MYSQL_PORT:-3306}
DB_MYSQL_USER: ${NPM_DB_MYSQL_USER}
DB_MYSQL_PASSWORD: ${NPM_DB_MYSQL_PASSWORD}
DB_MYSQL_NAME: ${NPM_DB_MYSQL_NAME}
volumes:
- proxymanager_data:/data
- proxymanager_letsencrypt:/etc/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
- db
db:
image: jc21/mariadb-aria:latest
container_name: proxymanager_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- proxymanager_db_data:/var/lib/mysql
rabbitmq:
image: rabbitmq:3-management
container_name: rabbitmq
restart: always
ports:
- "5672:5672" # AMQP protocol voor apps
- "15672:15672" # Management webinterface
environment:
- RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER}
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}
volumes:
- rabbitmq_data:/var/lib/rabbitmq
volumes:
rabbitmq_data:
node_red_data:
influxdb_data:
grafana_data:
jenkins_home:
gitea_data:
proxymanager_data:
proxymanager_letsencrypt:
proxymanager_db_data:

89
wiki/SCHEMA.md Normal file
View File

@@ -0,0 +1,89 @@
# Project Wiki Schema
## Purpose
LLM-maintained knowledge base for this project. The LLM writes and maintains everything. You read it (ideally in Obsidian). Knowledge compounds across sessions instead of being lost in chat history.
## Directory Structure
```
wiki/
SCHEMA.md — this file (how to maintain the wiki)
index.md — catalog of all pages with one-line summaries
log.md — chronological record of updates
overview.md — project overview and current status
metrics.md — all numbers with provenance
knowledge-graph.yaml — structured data, machine-queryable
tools/ — search, lint, query scripts
concepts/ — core ideas and mechanisms
architecture/ — design decisions, system internals
findings/ — honest results (what worked AND what didn't)
sessions/ — per-session summaries
```
## Page Conventions
### Frontmatter
Every page starts with YAML frontmatter:
```yaml
---
title: Page Title
created: YYYY-MM-DD
updated: YYYY-MM-DD
status: proven | disproven | evolving | speculative
tags: [tag1, tag2]
sources: [path/to/file.py, commit abc1234]
---
```
### Status values
- **proven**: tested and verified with evidence
- **disproven**: tested and honestly shown NOT to work (document WHY)
- **evolving**: partially working, boundary not fully mapped
- **speculative**: proposed but not yet tested
### Cross-references
Use `[[Page Name]]` Obsidian-style wikilinks.
### Contradictions
When new evidence contradicts a prior claim, DON'T delete the old claim. Add:
```
> [!warning] Superseded
> This was shown to be incorrect on YYYY-MM-DD. See [[New Finding]].
```
### Honesty rule
If something doesn't work, say so. If a result was a false positive, document how it was discovered. The wiki must be trustworthy.
## Operations
### Ingest (after a session or new source)
1. Read outputs, commits, findings
2. Update relevant pages
3. Create new pages for new concepts
4. Update `index.md`, `log.md`, `knowledge-graph.yaml`
5. Check for contradictions with existing pages
### Query
1. Use `python3 wiki/tools/query.py` for structured lookup
2. Use `wiki/tools/search.sh` for full-text
3. Read `index.md` to find relevant pages
4. File valuable answers back into the wiki
### Lint (periodically)
```bash
bash wiki/tools/lint.sh
```
Checks: orphan pages, broken wikilinks, missing frontmatter, index completeness.
## Data Layer
- `knowledge-graph.yaml` — structured YAML with every metric and data point
- `metrics.md` — human-readable dashboard
- When adding new results, update BOTH the wiki page AND the knowledge graph
- The knowledge graph is the single source of truth for numbers
## Source of Truth Hierarchy
1. **Test results** (actual outputs) — highest authority
2. **Code** (current state) — second authority
3. **Knowledge graph** (knowledge-graph.yaml) — structured metrics
4. **Wiki pages** — synthesis, may lag
5. **Chat/memory** — ephemeral, may be stale

View File

@@ -0,0 +1,56 @@
---
title: 3D Pump Curve Architecture
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [predict, curves, interpolation, rotatingMachine]
sources: [nodes/generalFunctions/src/predict/predict_class.js, nodes/rotatingMachine/src/specificClass.js]
---
# 3D Pump Curve Prediction
## Data Structure
A family of 2D curves indexed by pressure (f-dimension):
- **X-axis**: control position (0-100%)
- **Y-axis**: flow (nq) or power (np) in canonical units
- **F-dimension**: pressure (Pa) — the 3rd dimension
Raw curves are in curve units (m3/h, kW, mbar). `_normalizeMachineCurve()` converts to canonical (m3/s, W, Pa).
## Interpolation
Monotonic cubic spline (Fritsch-Carlson) in both dimensions:
- **X-Y splines**: at each discrete pressure level
- **F-splines**: across pressure levels for intermediate pressure interpolation
## Prediction Flow
```
predict.y(x):
1. Clamp x to [currentFxyXMin, currentFxyXMax]
2. Normalize x to [normMin, normMax]
3. Evaluate spline at normalized x for current fDimension
4. Return y in canonical units (m3/s or W)
```
## Unit Conversion Chain
```
Raw curve (m3/h, kW, mbar)
→ _normalizeMachineCurve → canonical (m3/s, W, Pa)
→ predict class → canonical output
→ MeasurementContainer.getCurrentValue(outputUnit) → output units
```
No double-conversion. Clean separation: specificClass handles units, predict handles normalization/interpolation.
## Three Predict Instances per Machine
- `predictFlow`: control % → flow (nq curve)
- `predictPower`: control % → power (np curve)
- `predictCtrl`: flow → control % (reversed nq curve)
## Boundary Behavior
- Below/above curve X range: flat extrapolation (clamped)
- Below/above f-dimension range: clamped to min/max pressure level
## Performance
- `y(x)`: O(log n), effectively O(1) for 5-10 data points
- `buildAllFxyCurves`: sub-10ms for typical curves
- Full caching of normalized curves, splines, and calculated curves

View File

@@ -1,3 +1,11 @@
---
title: EVOLV Deployment Blueprint
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [deployment, docker, edge, site, central]
---
# EVOLV Deployment Blueprint # EVOLV Deployment Blueprint
## Purpose ## Purpose

View File

@@ -0,0 +1,45 @@
---
title: Group Optimization Architecture
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [machineGroupControl, optimization, BEP-Gravitation]
sources: [nodes/machineGroupControl/src/specificClass.js]
---
# machineGroupControl Optimization
## Algorithm: BEP-Gravitation + Marginal-Cost Refinement
### Step 1 — Pressure Equalization
Sets all non-operational pumps to the group's max downstream / min upstream pressure. Ensures fair curve evaluation across combinations.
### Step 2 — Combination Enumeration
Generates all 2^n pump subsets (n = number of machines). Filters by:
- Machine state (excludes off, cooling, stopping, emergency)
- Mode compatibility (`execsequence` allowed in auto)
- Flow bounds: `sumMinFlow ≤ Qd ≤ sumMaxFlow`
- Optional power cap
### Step 3 — BEP-Gravitation Distribution (per combination)
1. **BEP seed**: `estimatedBEP = minFlow + span * NCog` per pump
2. **Slope estimation**: samples dP/dQ at BEP ± delta (directional: slopeLeft, slopeRight)
3. **Slope redistribution**: iteratively shifts flow from steep to flat curves (weight = 1/slope)
4. **Marginal-cost refinement**: after slope redistribution, shifts flow from highest actual dP/dQ to lowest using real `inputFlowCalcPower` evaluations. Converges regardless of curve convexity. Max 50 iterations, typically 5-15.
### Step 4 — Best Selection
Pick combination with lowest total power. Tiebreak by deviation from BEP.
### Step 5 — Execution
Start/stop pumps as needed, send `flowmovement` commands in output units via `_canonicalToOutputFlow()`.
## Three Control Modes
| Mode | Distribution | Combination Selection |
|------|-------------|----------------------|
| optimalControl | BEP-Gravitation + refinement | exhaustive 2^n |
| priorityControl | equal split, priority-ordered | sequential add/remove |
| priorityPercentageControl | percentage-based, normalized | count-based |
## Key Design Decision
The `flowmovement` command sends flow in the **machine's output units** (m3/h), not canonical (m3/s). The `_canonicalToOutputFlow()` helper converts before sending. Without this conversion, every pump stays at minimum flow (the critical bug fixed on 2026-04-07).

View File

@@ -1,3 +1,11 @@
---
title: EVOLV Architecture
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [architecture, node-red, three-layer]
---
# EVOLV Architecture # EVOLV Architecture
## 1. System Overview ## 1. System Overview

View File

@@ -1,3 +1,11 @@
---
title: EVOLV Platform Architecture
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [architecture, platform, edge-first]
---
# EVOLV Platform Architecture # EVOLV Platform Architecture
## At A Glance ## At A Glance

View File

@@ -1,3 +1,11 @@
---
title: EVOLV Architecture Review
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [architecture, stack, review]
---
# EVOLV Architecture Review # EVOLV Architecture Review
## Purpose ## Purpose

View File

@@ -1,3 +1,11 @@
---
title: generalFunctions API Reference
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [api, generalFunctions, reference]
---
# generalFunctions API Reference # generalFunctions API Reference
Shared library (`nodes/generalFunctions/`) used across all EVOLV Node-RED nodes. Shared library (`nodes/generalFunctions/`) used across all EVOLV Node-RED nodes.

View File

@@ -0,0 +1,38 @@
---
title: BEP-Gravitation Optimality Proof
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [machineGroupControl, optimization, BEP, brute-force]
sources: [nodes/machineGroupControl/test/integration/distribution-power-table.integration.test.js]
---
# BEP-Gravitation vs Brute-Force Global Optimum
## Claim
The machineGroupControl BEP-Gravitation algorithm (with marginal-cost refinement) produces near-optimal flow distribution across a pump group.
## Method
Brute-force exhaustive search: 1000 steps per pump, all 2^n combinations, 0.05% flow tolerance. Station: 2x H05K-S03R + 1x C5-D03R-SHN1 @ ΔP=2000 mbar.
## Results
| Demand | Brute force | machineGroupControl | Gap |
|--------|------------|--------------------|----|
| 10% (71 m3/h) | 17.65 kW | 17.63 kW | -0.10% (MGC wins) |
| 25% (136 m3/h) | 34.33 kW | 34.33 kW | +0.01% |
| 50% (243 m3/h) | 61.62 kW | 61.62 kW | -0.00% |
| 75% (351 m3/h) | 96.01 kW | 96.10 kW | +0.08% |
| 90% (415 m3/h) | 122.17 kW | 122.26 kW | +0.07% |
Maximum deviation: **0.1%** from proven global optimum.
## Why the Refinement Matters
Before the marginal-cost refinement loop, the gap at 50% demand was **2.12%**. The BEP-Gravitation slope estimate pushed 14.6 m3/h to C5 (costing 5.0 kW) when the optimum was 6.5 m3/h (0.59 kW). The refinement loop corrects this by shifting flow from highest actual dP/dQ to lowest until no improvement is possible.
## Stability
Sweep 5-95% in 2% steps: 1 switch (rising), 1 switch (falling), same transition point. No hysteresis. See [[Pump Switching Stability]].
## Computational Cost
0.027-0.153ms median per optimization call (3 pumps, 6 combinations). Uses 0.015% of the 1000ms tick budget.

View File

@@ -0,0 +1,34 @@
---
title: Pump Curve Non-Convexity
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [curves, interpolation, C5, non-convex]
sources: [nodes/generalFunctions/datasets/assetData/curves/hidrostal-C5-D03R-SHN1.json]
---
# Pump Curve Non-Convexity from Sparse Data
## Finding
The C5-D03R-SHN1 pump's power curve is non-convex after spline interpolation. The marginal cost (dP/dQ) shows a spike-then-valley pattern:
```
C5 dP/dQ across flow range @ ΔP=2000 mbar:
6.4 m3/h → 1,316,610 (high)
10.2 m3/h → 2,199,349 (spikes UP)
17.7 m3/h → 1,114,700 (dropping)
21.5 m3/h → 453,316 (valley — cheapest)
29.0 m3/h → 1,048,375 (rising again)
44.1 m3/h → 1,107,708 (high)
```
## Root Cause
The C5 curve has only **5 raw data points** per pressure level. The monotonic cubic spline (Fritsch-Carlson) creates a smooth curve through all 5 points, but with such sparse data it introduces non-convex regions that don't match the physical convexity of a real pump.
## Impact
- The equal-marginal-cost theorem (KKT conditions) does not apply — it requires convexity
- The BEP-Gravitation slope estimate at a single point can be misleading in non-convex regions
- The marginal-cost refinement loop fixes this by using actual power evaluations instead of slope assumptions
## Recommendation
Add more data points (15-20 per pressure level) to the C5 curve. This would make the spline track the real convex physics more closely, eliminating the non-convex artifacts.

View File

@@ -0,0 +1,42 @@
---
title: NCog Behavior and Limitations
created: 2026-04-07
updated: 2026-04-07
status: evolving
tags: [rotatingMachine, NCog, BEP, efficiency]
sources: [nodes/rotatingMachine/src/specificClass.js]
---
# NCog — Normalized Center of Gravity
## What It Is
NCog is a 0-1 value indicating where on its flow range a pump operates most efficiently. Computed per tick from the current pressure slice of the 3D pump curve.
```
BEP_flow = minFlow + (maxFlow - minFlow) * NCog
```
## How It's Computed
1. Pressure sensors update → `getMeasuredPressure()` computes differential
2. `fDimension` locks the 2D slice at current system pressure
3. `calcCog()` computes Q/P (specific flow) across the curve
4. Peak Q/P index → `NCog = (flowAtPeak - flowMin) / (flowMax - flowMin)`
## When NCog is Meaningful
NCog requires **differential pressure** (upstream + downstream). With only one pressure sensor, fDimension is the raw sensor value (too high), producing a monotonic Q/P curve and NCog = 0.
| Condition | NCog for H05K | NCog for C5 |
|-----------|--------------|-------------|
| ΔP = 400 mbar | 0.333 | 0.355 |
| ΔP = 1000 mbar | 0.000 | 0.000 |
| ΔP = 1500 mbar | 0.135 | 0.000 |
| ΔP = 2000 mbar | 0.351 | 0.000 |
## Why NCog = 0 Happens
For variable-speed centrifugal pumps, Q/P is monotonically decreasing when the affinity laws dominate (P ∝ Q³). At certain pressure levels, the spline interpolation preserves this monotonicity and the peak is always at index 0 (minimum flow).
## How the machineGroupControl Uses NCog
The BEP-Gravitation algorithm seeds each pump at its BEP flow, then redistributes using slope-based weights + marginal-cost refinement. Even when NCog = 0, the slope redistribution produces near-optimal results because it uses actual power evaluations.
> [!warning] Disproven: NCog as proportional weight
> Using NCog directly as a flow-distribution weight (`flow = NCog/totalNCog * Qd`) is wrong. It starves pumps with NCog = 0 and overloads high-NCog pumps. See `calcBestCombination` in machineGroupControl.

View File

@@ -1,3 +1,11 @@
---
title: Open Issues — EVOLV Codebase
created: 2026-03-01
updated: 2026-04-07
status: evolving
tags: [issues, backlog]
---
# Open Issues — EVOLV Codebase # Open Issues — EVOLV Codebase
Issues identified during codebase scan (2026-03-12). Create these on Gitea when ready. Issues identified during codebase scan (2026-03-12). Create these on Gitea when ready.

View File

@@ -0,0 +1,34 @@
---
title: Pump Switching Stability
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [machineGroupControl, stability, switching]
sources: [nodes/machineGroupControl/test/integration/ncog-distribution.integration.test.js]
---
# Pump Switching Stability
## Concern
Frequent pump on/off cycling causes mechanical wear, water hammer, and process disturbance.
## Test Method
Sweep demand from 5% to 95% in 2% steps, count combination changes. Repeat in reverse to check for hysteresis.
## Results — Mixed Station (2x H05K + 1x C5)
Rising 5→95%: **1 switch** at 27% (H05K-1+C5 → all 3)
Falling 95→5%: **1 switch** at 25% (all 3 → H05K-1+C5)
Same transition zone, no hysteresis.
## Results — Equal Station (3x H05K)
Rising 5→95%: **2 switches**
- 19%: 1 pump → 2 pumps
- 37%: 2 pumps → 3 pumps
Clean monotonic transitions, no flickering.
## Why It's Stable
The marginal-cost refinement only adjusts flow distribution WITHIN a combination — it never changes which pumps are selected. Combination selection is driven by total power comparison, which changes smoothly with demand.

48
wiki/index.md Normal file
View File

@@ -0,0 +1,48 @@
---
title: Wiki Index
updated: 2026-04-07
---
# EVOLV Project Wiki Index
## Overview
- [Project Overview](overview.md) — what works, what doesn't, node inventory
- [Metrics Dashboard](metrics.md) — test counts, power comparison, performance
- [Knowledge Graph](knowledge-graph.yaml) — structured data, machine-queryable
## Architecture
- [Node Architecture](architecture/node-architecture.md) — three-layer pattern, ports, mermaid diagrams
- [3D Pump Curves](architecture/3d-pump-curves.md) — predict class, spline interpolation, unit chain
- [Group Optimization](architecture/group-optimization.md) — BEP-Gravitation, combination selection, marginal-cost refinement
- [Platform Overview](architecture/platform-overview.md) — edge/site/central layering, telemetry model
- [Deployment Blueprint](architecture/deployment-blueprint.md) — Docker topology, rollout order
- [Stack Review](architecture/stack-review.md) — full stack architecture assessment
## Core Concepts
- [generalFunctions API](concepts/generalfunctions-api.md) — logger, MeasurementContainer, configManager, etc.
## Findings
- [BEP-Gravitation Proof](findings/bep-gravitation-proof.md) — within 0.1% of brute-force optimum (proven)
- [NCog Behavior](findings/ncog-behavior.md) — when NCog works, when it's zero, how it's used (evolving)
- [Curve Non-Convexity](findings/curve-non-convexity.md) — C5 sparse data artifacts (proven)
- [Pump Switching Stability](findings/pump-switching-stability.md) — 1-2 transitions, no hysteresis (proven)
- [Open Issues (2026-03)](findings/open-issues-2026-03.md) — diffuser, monster refactor, ML relocation, etc.
## Sessions
- [2026-04-07: Production Hardening](sessions/2026-04-07-production-hardening.md) — rotatingMachine + machineGroupControl
## Other Documentation (outside wiki)
- `CLAUDE.md` — Claude Code project guide (root)
- `AGENTS.md` — agent routing table, orchestrator policy (root, used by `.claude/agents/`)
- `.agents/` — skills, decisions, function-anchors, improvements
- `.claude/` — Claude Code agents and rules
- `manuals/node-red/` — FlowFuse dashboard and Node-RED reference docs
## Not Yet Documented
- Parent-child registration protocol (Port 2 handshake)
- Prediction health scoring algorithm (confidence 0-1)
- MeasurementContainer internals (chainable API, delta compression)
- PID controller implementation
- reactor / settler / monster / measurement / valve nodes
- pumpingStation node (uses rotatingMachine children)
- InfluxDB telemetry format (Port 1)

161
wiki/knowledge-graph.yaml Normal file
View File

@@ -0,0 +1,161 @@
# Knowledge Graph — structured data with provenance
# Every claim has: value, source (file/commit), date, status
# ── TESTS ──
tests:
rotatingMachine:
basic:
count: 10
passing: 10
file: nodes/rotatingMachine/test/basic/
date: 2026-04-07
integration:
count: 16
passing: 16
file: nodes/rotatingMachine/test/integration/
date: 2026-04-07
edge:
count: 17
passing: 17
file: nodes/rotatingMachine/test/edge/
date: 2026-04-07
machineGroupControl:
basic:
count: 1
passing: 1
file: nodes/machineGroupControl/test/basic/
date: 2026-04-07
integration:
count: 3
passing: 3
file: nodes/machineGroupControl/test/integration/
date: 2026-04-07
edge:
count: 1
passing: 1
file: nodes/machineGroupControl/test/edge/
date: 2026-04-07
# ── METRICS ──
metrics:
optimization_gap_to_brute_force:
value: "0.1% max"
source: distribution-power-table.integration.test.js
date: 2026-04-07
conditions: "3 pumps, 1000-step brute force, 0.05% flow tolerance"
optimization_time_median:
value: "0.027-0.153ms"
source: benchmark script
date: 2026-04-07
conditions: "3 pumps, 6 combinations, BEP-Gravitation + refinement"
pump_switching_stability:
value: "1-2 transitions across 5-95% demand"
source: stability sweep
date: 2026-04-07
conditions: "2% demand steps, both ascending and descending"
pump_curves:
H05K-S03R:
pressure_levels: 33
pressure_range: "700-3900 mbar"
flow_range: "28-227 m3/h (at 2000 mbar)"
data_points_per_level: 5
anomalies_fixed: 3
date: 2026-04-07
C5-D03R-SHN1:
pressure_levels: 26
pressure_range: "400-2900 mbar"
flow_range: "6-53 m3/h"
data_points_per_level: 5
non_convex: true
date: 2026-04-07
# ── DISPROVEN CLAIMS ──
disproven:
ncog_proportional_weight:
claimed: "Distributing flow proportional to NCog weights is optimal"
claimed_date: 2026-04-07
disproven_date: 2026-04-07
evidence_for: "Simple implementation in calcBestCombination"
evidence_against: "Starves small pumps (NCog=0 gets zero flow), overloads large pumps at high demand. BEP-target + scale is correct approach."
root_cause: "NCog is a position indicator (0-1 on flow range), not a distribution weight"
efficiency_rounding:
claimed: "Math.round(flow/power * 100) / 100 preserves BEP signal"
claimed_date: pre-2026-04-07
disproven_date: 2026-04-07
evidence_for: "Removes floating point noise"
evidence_against: "In canonical units (m3/s and W), Q/P ratio is ~1e-6. Rounding to 2 decimals produces 0 for all points. NCog, cog, BEP all became 0."
root_cause: "Canonical units make the ratio very small — rounding destroys the signal"
equal_marginal_cost_optimal:
claimed: "Equal dP/dQ across pumps guarantees global power minimum"
claimed_date: 2026-04-07
disproven_date: 2026-04-07
evidence_for: "KKT conditions for convex functions"
evidence_against: "C5 pump curve is non-convex (dP/dQ dips from 1.3M to 453K then rises). Sparse data (5 points) causes spline artifacts."
root_cause: "Convexity assumption fails with interpolated curves from sparse data"
# ── PERFORMANCE ──
performance:
mgc_optimization:
median_ms: 0.09
p99_ms: 0.5
tick_budget_pct: 0.015
source: benchmark script
date: 2026-04-07
predict_y_call:
complexity: "O(log n), ~O(1) for 5-10 data points"
source: predict_class.js
# ── ARCHITECTURE ──
architecture:
canonical_units:
pressure: Pa
flow: "m3/s"
power: W
temperature: K
output_units:
pressure: mbar
flow: "m3/h"
power: kW
temperature: C
node_count: 13
submodules: 12
# ── BUGS FIXED ──
bugs_fixed:
flowmovement_unit_mismatch:
severity: critical
description: "machineGroupControl sent flow in canonical (m3/s) but rotatingMachine flowmovement expected output units (m3/h). Every pump stayed at minimum."
fix: "_canonicalToOutputFlow() conversion before all flowmovement calls"
commit: d55f401
date: 2026-04-07
emergencystop_case:
severity: critical
description: "specificClass called executeSequence('emergencyStop') but config key was 'emergencystop'"
fix: "Lowercase to match config"
commit: 07af7ce
date: 2026-04-07
curve_data_anomalies:
severity: high
description: "3 flow values leaked into power column in hidrostal-H05K-S03R.json at pressures 1600, 3200, 3300 mbar"
fix: "Linearly interpolated correct values from adjacent levels"
commit: 024db55
date: 2026-04-07
efficiency_rounding:
severity: high
description: "Math.round(Q/P * 100) / 100 destroyed all NCog/BEP calculations"
fix: "Removed rounding, use raw ratio"
commit: 07af7ce
date: 2026-04-07
absolute_scaling_bug:
severity: high
description: "handleInput compared demandQout (always 0) instead of demandQ for max cap"
fix: "Reordered conditions, use demandQ throughout"
commit: d55f401
date: 2026-04-07
# ── TIMELINE ──
timeline:
- {date: 2026-04-07, commit: 024db55, desc: "Fix 3 anomalous power values in hidrostal curve"}
- {date: 2026-04-07, commit: 07af7ce, desc: "rotatingMachine production hardening: safety + prediction + 43 tests"}
- {date: 2026-04-07, commit: d55f401, desc: "machineGroupControl: unit fix + refinement + stability tests"}
- {date: 2026-04-07, commit: fd9d167, desc: "Update EVOLV submodule refs"}

11
wiki/log.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: Wiki Log
---
# Wiki Log
## [2026-04-07] Wiki initialized | Full codebase scan + session findings
- Created overview, metrics, knowledge graph from production hardening session
- Architecture pages: 3D pump curves, group optimization
- Findings: BEP-Gravitation proof, NCog behavior, curve non-convexity, switching stability
- Session log: 2026-04-07 production hardening

56
wiki/metrics.md Normal file
View File

@@ -0,0 +1,56 @@
---
title: Metrics Dashboard
updated: 2026-04-07
---
# Metrics Dashboard
All numbers with provenance. Source of truth: `knowledge-graph.yaml`.
## Test Results
| Suite | Pass/Total | File | Date |
|---|---|---|---|
| rotatingMachine basic | 10/10 | test/basic/*.test.js | 2026-04-07 |
| rotatingMachine integration | 16/16 | test/integration/*.test.js | 2026-04-07 |
| rotatingMachine edge | 17/17 | test/edge/*.test.js | 2026-04-07 |
| machineGroupControl basic | 1/1 | test/basic/*.test.js | 2026-04-07 |
| machineGroupControl integration | 3/3 | test/integration/*.test.js | 2026-04-07 |
| machineGroupControl edge | 1/1 | test/edge/*.test.js | 2026-04-07 |
## Performance — machineGroupControl Optimization
| Metric | Value | Source | Date |
|---|---|---|---|
| BEP-Gravitation + refinement (3 pumps, 6 combos) | 0.027-0.153ms median | benchmark script | 2026-04-07 |
| Tick loop budget used | 0.015% of 1000ms | benchmark script | 2026-04-07 |
| Max gap from brute-force optimum (1000 steps) | 0.1% | [[BEP Gravitation Proof]] | 2026-04-07 |
| Pump switching stability (5-95% sweep) | 1-2 transitions, no hysteresis | stability sweep | 2026-04-07 |
## Performance — rotatingMachine Prediction
| Metric | Value | Source |
|---|---|---|
| predict.y(x) call | O(log n), effectively O(1) | predict_class.js |
| buildAllFxyCurves | sub-10ms for typical curves | predict_class.js |
| Curve cache | full caching of splines + calculated curves | predict_class.js |
## Power Comparison: machineGroupControl vs Baselines
Station: 2x H05K-S03R + 1x C5-D03R-SHN1 @ ΔP=2000 mbar
| Demand | Qd (m3/h) | machineGroupControl | Spillover | Equal-all | Gap to optimum |
|--------|-----------|--------------------|-----------|-----------|----|
| 10% | 71 | 17.6 kW | 22.0 kW (+25%) | 23.9 kW (+36%) | -0.10% |
| 25% | 136 | 34.6 kW | 36.3 kW (+5%) | 39.1 kW (+13%) | +0.01% |
| 50% | 243 | 62.9 kW | 73.8 kW (+17%) | 64.2 kW (+2%) | -0.00% |
| 75% | 351 | 96.8 kW | 102.9 kW (+6%) | 99.6 kW (+3%) | +0.08% |
| 90% | 415 | 122.8 kW | 123.0 kW (0%) | 123.0 kW (0%) | +0.07% |
## Disproven Claims
| Claim | Evidence For | Evidence Against | Date |
|---|---|---|---|
| NCog as proportional weight works | Simple implementation | Starves small pumps, overloads large ones at high demand | 2026-04-07 |
| Q/P ratio always has mid-range peak | Expected from pump physics | Monotonically decreasing at high ΔP due to affinity laws (P ∝ Q³) | 2026-04-07 |
| Equal-marginal-cost solver is optimal | KKT theory for convex curves | C5 curve is non-convex due to sparse data points (5 per pressure) | 2026-04-07 |

70
wiki/overview.md Normal file
View File

@@ -0,0 +1,70 @@
---
title: EVOLV Project Overview
created: 2026-04-07
updated: 2026-04-07
status: evolving
tags: [overview, wastewater, node-red, isa-88]
---
# EVOLV — Edge-Layer Evolution for Optimized Virtualization
Industrial automation platform for wastewater treatment, built as custom Node-RED nodes by Waterschap Brabantse Delta R&D. Follows ISA-88 (S88) batch control standard.
## Stack
Node.js, Node-RED, InfluxDB (time-series), TensorFlow.js (prediction), CoolProp (thermodynamics). No build step — pure Node.js.
## Architecture
Each node follows a 3-tier pattern:
1. **Entry file** — registers with Node-RED, admin HTTP endpoints
2. **nodeClass** — Node-RED adapter (tick loop, message routing, status)
3. **specificClass** — pure domain logic (physics, state machines, predictions)
3-port output convention: Port 0 = process data, Port 1 = InfluxDB telemetry, Port 2 = parent-child registration.
## What Works
| Capability | Status | Evidence |
|---|---|---|
| rotatingMachine state machine | proven | 76 tests passing, all sequences verified |
| 3D pump curve prediction (flow/power from pressure+control) | proven | Monotonic cubic spline interpolation across 34 pressure levels |
| NCog / BEP tracking per pump | proven | Produces meaningful values with differential pressure |
| machineGroupControl BEP-Gravitation | proven | Within 0.1% of brute-force global optimum |
| Combination selection (2^n exhaustive) | proven | Stable: 1-2 switches across 5-95% demand sweep, no hysteresis |
| Prediction health scoring | proven | NRMSE drift, pressure source penalties, edge detection |
| Hydraulic efficiency (η = QΔP/P) | proven | CoolProp density, head calculation |
| Unit conversion chain | proven | No double-conversion, clean layer separation |
## What Doesn't Work (honestly)
| Issue | Status | Evidence |
|---|---|---|
| C5 curve non-convexity | evolving | 5 raw data points cause spline artifacts, dP/dQ non-monotonic |
| NCog = 0 at high ΔP | evolving | At ΔP > 800 mbar for H05K, Q/P is monotonically decreasing |
| calcBestCombination (NCog-weight mode) | disproven | Uses NCog as proportional weight instead of BEP target |
## Current Scale
- 13 custom Node-RED nodes (12 submodules + generalFunctions)
- rotatingMachine: 76 tests, 1563 lines domain logic
- machineGroupControl: 90+ tests, 1400+ lines domain logic
- 3 real pump curves: H05K-S03R, C5-D03R-SHN1, ECDV
- Tick loop: 1000ms interval
## Node Inventory
| Node | Purpose | Test Status |
|------|---------|-------------|
| rotatingMachine | Pump/compressor control | 76 tests (full) |
| machineGroupControl | Multi-pump optimization | 90 tests (full) |
| pumpingStation | Multi-pump station | needs review |
| valve | Valve modeling | needs review |
| valveGroupControl | Valve group coordination | needs review |
| reactor | Biological reactor (ASM kinetics) | needs review |
| settler | Secondary clarifier | needs review |
| monster | Multi-parameter bio monitoring | needs review |
| measurement | Sensor signal conditioning | needs review |
| diffuser | Aeration system control | needs review |
| dashboardAPI | InfluxDB + FlowFuse charts | needs review |
| generalFunctions | Shared utilities | partial |

View File

@@ -0,0 +1,46 @@
---
title: "Session: Production Hardening rotatingMachine + machineGroupControl"
created: 2026-04-07
updated: 2026-04-07
status: proven
tags: [session, rotatingMachine, machineGroupControl, testing]
---
# 2026-04-07 — Production Hardening
## Scope
Full code review and hardening of rotatingMachine and machineGroupControl nodes for production readiness.
## Key Discoveries
1. **Efficiency rounding destroyed NCog/BEP**`Math.round(Q/P * 100) / 100` in canonical units (m3/s and W) produces ratios ~1e-6 that all round to 0. All NCog, cog, and BEP calculations were non-functional. Fixed by removing rounding.
2. **flowmovement unit mismatch** — machineGroupControl computed flow in canonical (m3/s) and sent it directly to rotatingMachine which expected output units (m3/h). Every pump stayed at minimum flow. Fixed with `_canonicalToOutputFlow()`.
3. **emergencyStop case mismatch**`"emergencyStop"` vs config key `"emergencystop"`. Emergency stop never worked. Fixed to lowercase.
4. **Curve data anomalies** — 3 flow values leaked into power columns in hidrostal-H05K-S03R.json at pressures 1600, 3200, 3300 mbar. Fixed with interpolated values.
5. **C5 pump non-convexity** — 5 data points per pressure level produces non-convex spline. The marginal-cost refinement loop closes the gap to brute-force optimum from 2.1% to 0.1%.
## Changes Made
### rotatingMachine (3 files, 7 test files)
- Async input handler, null guards, listener cleanup, tick loop race fix
- showCoG() implementation, efficiency variant fix, curve anomaly detection
- 43 new tests (76 total)
### machineGroupControl (1 file, 2 test files)
- `_canonicalToOutputFlow()` on all flowmovement calls
- Absolute scaling bug, empty Qd block, empty-machines guards
- Marginal-cost refinement loop in BEP-Gravitation
- Missing flowmovement after startup in equalFlowControl
### generalFunctions (1 file)
- 3 curve data fixes in hidrostal-H05K-S03R.json
## Verification
- 90 tests passing across both nodes
- machineGroupControl within 0.1% of brute-force global optimum (1000-step search)
- Pump switching stable: 1-2 transitions across full demand range, no hysteresis
- Optimization cost: 0.03-0.15ms per call (0.015% of tick budget)

46
wiki/tools/lint.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
# Wiki health check — find issues
# Usage: ./wiki/tools/lint.sh
WIKI_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
echo "=== Wiki Health Check ==="
echo ""
echo "-- Page count --"
find "$WIKI_DIR" -name "*.md" -not -path "*/tools/*" | wc -l
echo " total pages"
echo ""
echo "-- Orphans (not linked from other pages) --"
for f in $(find "$WIKI_DIR" -name "*.md" -not -name "index.md" -not -name "log.md" -not -name "SCHEMA.md" -not -path "*/tools/*"); do
basename=$(basename "$f" .md)
refs=$(grep -rl --include="*.md" "$basename" "$WIKI_DIR" 2>/dev/null | grep -v "$f" | wc -l)
if [ "$refs" -eq 0 ]; then
echo " ORPHAN: $f"
fi
done
echo ""
echo "-- Status distribution --"
for status in proven disproven evolving speculative; do
count=$(grep -rl "status: $status" "$WIKI_DIR" --include="*.md" 2>/dev/null | wc -l)
echo " $status: $count"
done
echo ""
echo "-- Pages missing frontmatter --"
for f in $(find "$WIKI_DIR" -name "*.md" -not -name "SCHEMA.md" -not -path "*/tools/*"); do
if ! head -1 "$f" | grep -q "^---"; then
echo " NO FRONTMATTER: $f"
fi
done
echo ""
echo "-- Index completeness --"
indexed=$(grep -c '\[.*\](.*\.md)' "$WIKI_DIR/index.md" 2>/dev/null)
total=$(find "$WIKI_DIR" -name "*.md" -not -name "index.md" -not -name "log.md" -not -name "SCHEMA.md" -not -path "*/tools/*" | wc -l)
echo " Indexed: $indexed / Total: $total"
echo ""
echo "=== Done ==="

249
wiki/tools/query.py Normal file
View File

@@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""Wiki Knowledge Graph query tool.
Queryable interface over knowledge-graph.yaml + wiki pages.
Usable by both humans (CLI) and LLM agents (imported).
Usage:
python wiki/tools/query.py health # project health
python wiki/tools/query.py entity "search term" # everything about an entity
python wiki/tools/query.py metric "search term" # find metrics
python wiki/tools/query.py status "proven" # all pages with status
python wiki/tools/query.py test "test name" # test results
python wiki/tools/query.py search "keyword" # full-text search
python wiki/tools/query.py related "page-name" # pages linking to/from
python wiki/tools/query.py timeline # commit timeline
"""
import yaml
import os
import sys
import re
from pathlib import Path
WIKI_DIR = Path(__file__).parent.parent
GRAPH_PATH = WIKI_DIR / 'knowledge-graph.yaml'
def load_graph():
if not GRAPH_PATH.exists():
return {}
with open(GRAPH_PATH) as f:
return yaml.safe_load(f) or {}
def load_all_pages():
pages = {}
for md_path in WIKI_DIR.rglob('*.md'):
if 'tools' in str(md_path):
continue
rel = md_path.relative_to(WIKI_DIR)
content = md_path.read_text()
meta = {}
if content.startswith('---'):
parts = content.split('---', 2)
if len(parts) >= 3:
try:
meta = yaml.safe_load(parts[1]) or {}
except yaml.YAMLError:
pass
content = parts[2]
links = re.findall(r'\[\[([^\]]+)\]\]', content)
pages[str(rel)] = {
'path': str(rel), 'meta': meta, 'content': content,
'links': links, 'title': meta.get('title', str(rel)),
'status': meta.get('status', 'unknown'),
'tags': meta.get('tags', []),
}
return pages
def flatten_graph(graph, prefix=''):
items = []
if isinstance(graph, dict):
for k, v in graph.items():
path = f"{prefix}.{k}" if prefix else k
if isinstance(v, (dict, list)):
items.extend(flatten_graph(v, path))
else:
items.append((path, str(v)))
elif isinstance(graph, list):
for i, v in enumerate(graph):
path = f"{prefix}[{i}]"
if isinstance(v, (dict, list)):
items.extend(flatten_graph(v, path))
else:
items.append((path, str(v)))
return items
def cmd_health():
graph = load_graph()
pages = load_all_pages()
statuses = {}
for p in pages.values():
s = p['status']
statuses[s] = statuses.get(s, 0) + 1
tests = graph.get('tests', {})
total_pass = sum(t.get('passing', 0) for t in tests.values() if isinstance(t, dict))
total_count = sum(t.get('count', t.get('total', 0)) for t in tests.values() if isinstance(t, dict))
disproven = len(graph.get('disproven', {}))
timeline = len(graph.get('timeline', []))
# Count broken links
all_titles = set()
for p in pages.values():
all_titles.add(p['title'].lower())
all_titles.add(p['path'].lower().replace('.md', '').split('/')[-1])
broken = sum(1 for p in pages.values() for link in p['links']
if not any(link.lower().replace('-', ' ') in t or t in link.lower().replace('-', ' ')
for t in all_titles))
print(f"Wiki Health:\n")
print(f" Pages: {len(pages)}")
print(f" Statuses: {statuses}")
if total_count:
print(f" Tests: {total_pass}/{total_count} passing")
print(f" Disproven: {disproven} claims tracked")
print(f" Timeline: {timeline} commits")
print(f" Broken links: {broken}")
def cmd_entity(query):
graph = load_graph()
pages = load_all_pages()
q = query.lower()
print(f"Entity: '{query}'\n")
flat = flatten_graph(graph)
hits = [(p, v) for p, v in flat if q in p.lower() or q in v.lower()]
if hits:
print(" -- Knowledge Graph --")
for path, value in hits[:20]:
print(f" {path}: {value}")
print("\n -- Wiki Pages --")
for rel, page in sorted(pages.items()):
if q in page['content'].lower() or q in page['title'].lower():
lines = [l.strip() for l in page['content'].split('\n')
if q in l.lower() and l.strip()]
print(f" {rel} ({page['status']})")
for line in lines[:3]:
print(f" {line[:100]}")
def cmd_metric(query):
flat = flatten_graph(load_graph())
q = query.lower()
print(f"Metrics matching '{query}':\n")
found = 0
for path, value in flat:
if q in path.lower() or q in value.lower():
print(f" {path}: {value}")
found += 1
if not found:
print(" (no matches)")
def cmd_status(status):
pages = load_all_pages()
graph = load_graph()
print(f"Status: '{status}'\n")
for rel, page in sorted(pages.items()):
if page['status'] == status:
print(f" {page['title']} ({rel})")
if page['tags']:
print(f" tags: {page['tags']}")
if status == 'disproven' and 'disproven' in graph:
print("\n -- Disproven Claims --")
for name, claim in graph['disproven'].items():
print(f" {name}:")
for k, v in claim.items():
print(f" {k}: {v}")
def cmd_test(query):
tests = load_graph().get('tests', {})
q = query.lower()
print(f"Test results for '{query}':\n")
for name, suite in tests.items():
if q in name.lower() or q in str(suite).lower():
print(f" -- {name} --")
if isinstance(suite, dict):
for k, v in suite.items():
if isinstance(v, dict):
print(f" {k}: {v.get('passing', '?')}/{v.get('total', '?')}")
elif k in ('count', 'passing', 'accuracy', 'file', 'date'):
print(f" {k}: {v}")
elif k == 'results' and isinstance(v, list):
for r in v:
mark = '' if r.get('result') == 'pass' else ''
print(f" {mark} {r.get('test', '?')}")
def cmd_search(query):
flat = flatten_graph(load_graph())
pages = load_all_pages()
q = query.lower()
print(f"Search: '{query}'\n")
graph_hits = [(p, v) for p, v in flat if q in v.lower()]
if graph_hits:
print(f" -- Knowledge Graph ({len(graph_hits)} hits) --")
for p, v in graph_hits[:10]:
print(f" {p}: {v[:80]}")
page_hits = sorted(
[(page['content'].lower().count(q), rel, page['title'])
for rel, page in pages.items() if q in page['content'].lower()],
reverse=True)
if page_hits:
print(f"\n -- Wiki Pages ({len(page_hits)} pages) --")
for count, rel, title in page_hits:
print(f" {count:3d}x {title} ({rel})")
def cmd_related(page_name):
pages = load_all_pages()
q = page_name.lower().replace('-', ' ').replace('_', ' ')
print(f"Related to: '{page_name}'\n")
print(" -- Links TO --")
for rel, page in sorted(pages.items()):
for link in page['links']:
if q in link.lower().replace('-', ' '):
print(f" <- {page['title']} ({rel})")
break
print("\n -- Links FROM --")
for rel, page in pages.items():
if q in page['title'].lower().replace('-', ' '):
for link in page['links']:
print(f" -> [[{link}]]")
break
def cmd_timeline():
for entry in load_graph().get('timeline', []):
print(f" [{entry.get('date')}] {entry.get('commit', '?')}: {entry.get('desc', '?')}")
COMMANDS = {
'health': cmd_health, 'entity': cmd_entity, 'metric': cmd_metric,
'status': cmd_status, 'test': cmd_test, 'search': cmd_search,
'related': cmd_related, 'timeline': cmd_timeline,
}
if __name__ == '__main__':
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
print(f"Usage: query.py <{'|'.join(COMMANDS)}> [args]")
sys.exit(1)
cmd = sys.argv[1]
args = sys.argv[2:]
if cmd in ('timeline', 'health'):
COMMANDS[cmd]()
elif args:
COMMANDS[cmd](' '.join(args))
else:
print(f"Usage: query.py {cmd} <query>")

18
wiki/tools/search.sh Normal file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Search the wiki — usable by both humans and LLM agents
# Usage: ./wiki/tools/search.sh "query" [--files-only]
WIKI_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
QUERY="$1"
MODE="${2:---content}"
if [ -z "$QUERY" ]; then
echo "Usage: $0 <query> [--files-only]"
exit 1
fi
if [ "$MODE" = "--files-only" ]; then
grep -rl --include="*.md" --include="*.yaml" "$QUERY" "$WIKI_DIR" 2>/dev/null | sort
else
grep -rn --include="*.md" --include="*.yaml" --color=auto -i "$QUERY" "$WIKI_DIR" 2>/dev/null
fi