agentic-kdd 3.5.6 → 3.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,68 +1,153 @@
1
- # agentic-kdd CLI
1
+ # Agentic KDD v3.6
2
2
 
3
- Install [Agentic KDD](https://github.com/Adrianlpz211/Agentic-KDD) in any project with one command.
3
+ > Autonomous development pipeline with persistent knowledge memory. Forces TDD, blocks spec violations before build, and eliminates repeated errors across sessions.
4
4
 
5
- ## Install
5
+ [![npm](https://img.shields.io/npm/v/agentic-kdd)](https://www.npmjs.com/package/agentic-kdd)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ ---
9
+
10
+ ## What it is
11
+
12
+ Agentic KDD is a framework that runs inside **Cursor** and **Claude Code**. It turns your AI assistant into a disciplined development pipeline with:
13
+
14
+ - **KDD Memory** — errors, patterns and decisions persist across sessions. The agent reads them before every task.
15
+ - **TDD Gate** — deterministic self-healing loop. Forces tests to pass before continuing. Not a suggestion — it's code.
16
+ - **Spec Gate** — blocks changes that contradict documented business rules before writing a single line.
17
+ - **Security Gate** — detects JWT bypass, cross-tenant access and debug flags before build.
18
+ - **Regression Guard** — protects healthy behaviors across cascading file changes.
19
+ - **Contract Guard** — tracks passing test patterns and escalates them to protected contracts over time.
20
+
21
+ ---
22
+
23
+ ## Benchmark results (19 phases, Node.js + Python stacks)
24
+
25
+ | Metric | Without | With | Change |
26
+ |---|---|---|---|
27
+ | Errors per phase | 2.6 | 0.0 | ✅ 100% |
28
+ | Repeated errors | 3 | 0 | ✅ 100% |
29
+ | Tests passing first try | 79% | 100% | ✅ +27pp |
30
+ | Spec drift detections | 3 | 6 | ✅ +100% |
31
+ | Cascade files correct | 4 | 11 | ✅ +175% |
32
+ | Security issues detected | 1 | 2 | ✅ +100% |
33
+ | Autonomous fixes | 4 | 7 | ✅ +75% |
34
+
35
+ > N=1 project per benchmark. Treat as directional, not definitive. Reproducible benchmark repo in progress.
36
+
37
+ ---
38
+
39
+ ## Installation
6
40
 
7
41
  ```bash
8
42
  npm install -g agentic-kdd
9
43
  ```
10
44
 
11
- ## Usage
45
+ Requires Node.js 18+.
46
+
47
+ ---
12
48
 
13
- ### Initialize a project
49
+ ## Setup
14
50
 
15
51
  ```bash
16
52
  cd your-project
17
53
  akdd init
18
54
  ```
19
55
 
20
- Detects your stack, asks 3 questions, and installs Agentic KDD.
21
- Then open the project in Cursor or Claude Code and type:
56
+ That's it. Agentic KDD detects your stack (Node.js, Python, PHP), installs `.agentic/`, injects the `dev:kdd` script, and configures the pipeline.
22
57
 
58
+ For brownfield projects (existing codebase):
59
+
60
+ ```bash
61
+ akdd onboard
23
62
  ```
24
- aa: [your task]
63
+
64
+ Scans your project, maps the stack, suggests a first small task, and pre-populates memory with what it finds.
65
+
66
+ ---
67
+
68
+ ## Usage
69
+
70
+ Every task goes through `aa:`:
71
+
72
+ ```
73
+ aa: implement the clients module with CRUD and tenant isolation
74
+ ```
75
+
76
+ The pipeline runs automatically:
77
+
25
78
  ```
79
+ ① Analyst — reads memory, recalls patterns and past errors
80
+ ② Spec Gate — blocks if prompt contradicts HIGH-confidence rules
81
+ ③ Security Gate — checks CRITICAL/SENSITIVE files before build
82
+ ④ Regression Check — verifies changeset won't break protected behaviors
83
+ ⑤ Build — implementation
84
+ ⑥ TDD Gate — runs tests, self-heals up to 3 iterations
85
+ ⑦ QA — validates against spec and memory
86
+ ⑧ Preservation Gate — verifies contracts
87
+ ⑨ Regression Register — snapshots healthy behaviors
88
+ ⑩ Memory — writes errors, patterns and decisions
89
+ ⑪ Creative — detects improvement opportunities
90
+ ```
91
+
92
+ ---
26
93
 
27
- ### Update to latest version
94
+ ## CLI commands
28
95
 
29
96
  ```bash
30
- akdd update
97
+ akdd init # Install in a new project
98
+ akdd onboard # Analyze an existing project (brownfield)
99
+ akdd update # Pull latest from GitHub
100
+ akdd contracts # Contract Guard status
101
+ akdd contracts list # List all contracts
102
+ akdd graph # Knowledge graph state
103
+ akdd metrics # Project metrics
104
+ akdd health # System health check
105
+ akdd analyze # Cross-artifact consistency check
31
106
  ```
32
107
 
33
- Updates agent instructions to the latest version.
34
- Your memory, config, and knowledge base are never touched.
108
+ ---
35
109
 
36
- ## What gets installed
110
+ ## What's in the repo
37
111
 
38
112
  ```
39
- your-project/
40
- ├── CLAUDE.md ← Claude Code reads this automatically
41
- ├── _LOCKS.md ← parallel instance coordination
42
- ├── _output/ auto-generated decision logs
43
- ├── .cursor/rules/agentic.mdc ← Cursor reads this automatically
44
- └── .agentic/
45
- ├── config.md ← auto-configured for your stack
46
- ├── PLAN.md active task tracker
47
- ├── memoria/ KDD knowledge base
48
- │ ├── trabajo.md
49
- │ ├── errores.md
50
- │ ├── patrones.md
51
- │ └── decisiones.md
52
- ├── agentes/ ← 8 agent instruction files
53
- └── conocimiento/ ← drop your project docs here
113
+ .agentic/
114
+ ├── agentes/ # Role instructions (Orchestrator, Analyst, Back, Front, QA, Memory)
115
+ ├── grafo/ # Gate modules (tdd-gate, spec-gate, security-gate, regression-guard, contract-guard)
116
+ ├── memoria/ # KDD memory files (errors, patterns, decisions, work)
117
+ ├── config.md # Project configuration
118
+ └── PLAN.md # Active task
119
+
120
+ CLAUDE.md # Pipeline rules for Claude Code
121
+ .cursor/rules/ # Pipeline rules for Cursor
54
122
  ```
55
123
 
56
- ## Requirements
124
+ ---
125
+
126
+ ## How it compares
127
+
128
+ | | OpenSpec | Spec-Kit | Agentic KDD |
129
+ |---|---|---|---|
130
+ | Spec violation gate | ❌ | ❌ | ✅ blocks before build |
131
+ | Security gate | ❌ | ❌ | ✅ blocks before build |
132
+ | Error memory (KDD) | ❌ | ❌ | ✅ persists across sessions |
133
+ | TDD self-healing loop | ❌ | Partial | ✅ deterministic, 3 iterations |
134
+ | Regression protection | ❌ | ❌ | ✅ cascade-tested |
135
+ | Contract escalation | ❌ | ❌ | ✅ candidate → verified → protected |
136
+ | Python support | ❌ | ✅ | ✅ pytest + FastAPI |
137
+ | Brownfield onboarding | ✅ | Partial | ✅ akdd onboard |
138
+ | npm install | ✅ | ❌ | ✅ |
139
+ | MCP tools | 25+ | — | 59 |
140
+
141
+ ---
57
142
 
58
- - Node.js 18+
59
- - curl (pre-installed on Mac/Linux; on Windows use Git Bash)
143
+ ## Stack support
60
144
 
61
- ## Links
145
+ - **Node.js** — Next.js, Express, Fastify, NestJS + Jest/Vitest
146
+ - **Python** — FastAPI, Django, Flask + pytest
147
+ - **PHP** — Laravel + PHPUnit
62
148
 
63
- - [GitHub](https://github.com/Adrianlpz211/Agentic-KDD)
64
- - [Documentation](https://github.com/Adrianlpz211/Agentic-KDD/blob/main/docs/kdd-methodology.md)
149
+ ---
65
150
 
66
151
  ## License
67
152
 
68
- MIT
153
+ MIT — Adrián López ([@Adrianlpz211](https://github.com/Adrianlpz211))
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+ /**
3
+ * Agentic KDD — Analyzer v1.0
4
+ * Verificación de consistencia cross-artefacto.
5
+ * Compara specs, código y tests para detectar inconsistencias antes de que
6
+ * el agente las encuentre en producción.
7
+ *
8
+ * Uso:
9
+ * node .agentic/grafo/akdd-analyze.cjs run → análisis completo
10
+ * node .agentic/grafo/akdd-analyze.cjs contracts → verifica contratos vs tests
11
+ * node .agentic/grafo/akdd-analyze.cjs memory → verifica memoria vs código
12
+ * node .agentic/grafo/akdd-analyze.cjs spec → verifica spec vs código
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const ROOT = process.cwd();
19
+ const AGENTIC_DIR = path.join(ROOT, '.agentic');
20
+ const MEMORIA_DIR = path.join(AGENTIC_DIR, 'memoria');
21
+ const CONFIG_FILE = path.join(AGENTIC_DIR, 'config.md');
22
+ const SPECS_DIR = path.join(AGENTIC_DIR, 'specs');
23
+
24
+ // ── Findings ──────────────────────────────────────────────────────────────────
25
+
26
+ const findings = [];
27
+
28
+ function addFinding(severity, category, message, suggestion) {
29
+ findings.push({ severity, category, message, suggestion });
30
+ }
31
+
32
+ // ── Check 1: Contratos vs test files ─────────────────────────────────────────
33
+
34
+ function checkContractsVsTests() {
35
+ // Open the SQLite DB if available
36
+ const dbPath = path.join(AGENTIC_DIR, 'memoria.db');
37
+ if (!require('fs').existsSync(dbPath)) {
38
+ addFinding('INFO', 'contracts', 'memoria.db no encontrada — sin contratos que verificar', 'Corre akdd init y algunos ciclos aa:');
39
+ return;
40
+ }
41
+
42
+ let db;
43
+ try {
44
+ const projNodeModules = path.join(ROOT, 'node_modules');
45
+ if (!module.paths.includes(projNodeModules)) module.paths.unshift(projNodeModules);
46
+ db = new (require('better-sqlite3'))(dbPath);
47
+ } catch(e) {
48
+ addFinding('WARN', 'contracts', 'No se pudo abrir memoria.db: ' + e.message, 'Verifica que better-sqlite3 está instalado');
49
+ return;
50
+ }
51
+
52
+ try {
53
+ const contracts = db.prepare("SELECT * FROM verified_contracts WHERE status != 'invalidated'").all();
54
+
55
+ if (contracts.length === 0) {
56
+ addFinding('INFO', 'contracts', 'Sin contratos registrados aún', 'Corre ciclos aa: para acumular contratos');
57
+ return;
58
+ }
59
+
60
+ // Check each contract's test file still exists
61
+ for (const c of contracts) {
62
+ if (c.test_file && c.test_file !== 'npm test' && c.test_file !== 'pytest') {
63
+ const testPath = path.join(ROOT, c.test_file);
64
+ if (!fs.existsSync(testPath)) {
65
+ addFinding('HIGH', 'contracts',
66
+ `Contrato [${c.id}] referencia "${c.test_file}" que ya no existe`,
67
+ `Corre akdd contracts verify para actualizar el contrato`);
68
+ }
69
+ }
70
+
71
+ // Warn on stale protected contracts
72
+ if (c.status === 'protected' && c.last_verified) {
73
+ const days = Math.floor((Date.now() - new Date(c.last_verified).getTime()) / 86400000);
74
+ if (days > 30) {
75
+ addFinding('WARN', 'contracts',
76
+ `Contrato PROTECTED [${c.module}] no verificado en ${days} días`,
77
+ `Corre akdd contracts gate para re-verificar`);
78
+ }
79
+ }
80
+ }
81
+
82
+ const protected_ = contracts.filter(c => c.status === 'protected').length;
83
+ const verified = contracts.filter(c => c.status === 'verified').length;
84
+ addFinding('INFO', 'contracts',
85
+ `${contracts.length} contratos: ${protected_} PROTECTED, ${verified} VERIFIED`,
86
+ null);
87
+
88
+ } catch(e) {
89
+ addFinding('WARN', 'contracts', 'Error leyendo contratos: ' + e.message, null);
90
+ } finally {
91
+ db.close();
92
+ }
93
+ }
94
+
95
+ // ── Check 2: Memoria vs código actual ────────────────────────────────────────
96
+
97
+ function checkMemoryVsCode() {
98
+ const erroresFile = path.join(MEMORIA_DIR, 'errores.md');
99
+ const patronesFile= path.join(MEMORIA_DIR, 'patrones.md');
100
+
101
+ if (!fs.existsSync(erroresFile) && !fs.existsSync(patronesFile)) {
102
+ addFinding('INFO', 'memory', 'Archivos de memoria no encontrados', 'Corre akdd init');
103
+ return;
104
+ }
105
+
106
+ // Check patrones.md for HIGH-confidence rules and verify they're not violated in code
107
+ if (fs.existsSync(patronesFile)) {
108
+ const patrones = fs.readFileSync(patronesFile, 'utf8');
109
+ const highPatterns = patrones.match(/###[^\n]+\n[\s\S]*?\*\*confianza\*\*:\s*ALTA[\s\S]*?(?=###|$)/gi) || [];
110
+
111
+ // Basic structural checks on source files
112
+ const srcFiles = findSourceFiles(ROOT);
113
+
114
+ for (const pattern of highPatterns) {
115
+ const titleMatch = pattern.match(/###\s+(.+)/);
116
+ const ruleMatch = pattern.match(/\*\*regla\*\*:\s*(.+)/i);
117
+ if (!titleMatch || !ruleMatch) continue;
118
+
119
+ const rule = ruleMatch[1].toLowerCase();
120
+
121
+ // Check: if rule mentions "tenant" or "agency_id", verify files filter by it
122
+ if ((rule.includes('tenant') || rule.includes('agency_id')) && srcFiles.length > 0) {
123
+ const routeFiles = srcFiles.filter(f => f.includes('route') || f.includes('router') || f.includes('api'));
124
+ const violations = routeFiles.filter(f => {
125
+ const content = safeRead(f);
126
+ return content && content.includes('findMany') && !content.includes('agency_id') &&
127
+ !content.includes('tenant') && !content.includes('TESTING');
128
+ });
129
+ if (violations.length > 0) {
130
+ addFinding('HIGH', 'memory',
131
+ `Patrón HIGH "${titleMatch[1]}" puede estar violado en: ${violations.slice(0,3).map(f => path.relative(ROOT,f)).join(', ')}`,
132
+ `Revisar manualmente — patrón: ${rule}`);
133
+ }
134
+ }
135
+ }
136
+
137
+ addFinding('INFO', 'memory', `${highPatterns.length} patrones HIGH verificados contra código`, null);
138
+ }
139
+
140
+ // Check errores.md size
141
+ if (fs.existsSync(erroresFile)) {
142
+ const errLines = fs.readFileSync(erroresFile, 'utf8').split('\n').filter(l => l.startsWith('### ')).length;
143
+ if (errLines > 50) {
144
+ addFinding('WARN', 'memory',
145
+ `errores.md tiene ${errLines} entradas — puede degradar el contexto del agente`,
146
+ `Corre: node .agentic/grafo/mem-curator.cjs run`);
147
+ }
148
+ }
149
+ }
150
+
151
+ // ── Check 3: Config vs stack real ────────────────────────────────────────────
152
+
153
+ function checkConfigVsStack() {
154
+ if (!fs.existsSync(CONFIG_FILE)) {
155
+ addFinding('HIGH', 'config', 'config.md no encontrado', 'Corre akdd init');
156
+ return;
157
+ }
158
+
159
+ const config = fs.readFileSync(CONFIG_FILE, 'utf8');
160
+
161
+ // Check test command is configured
162
+ const testMatch = config.match(/^\s*test:\s*(.+)$/m);
163
+ if (!testMatch || testMatch[1].trim() === '—' || testMatch[1].trim() === '') {
164
+ addFinding('HIGH', 'config',
165
+ 'Comando de tests no configurado en config.md',
166
+ 'Agrega: test: npm test (o el comando de tu stack)');
167
+ } else {
168
+ addFinding('INFO', 'config', `Comando de tests: ${testMatch[1].trim()}`, null);
169
+ }
170
+
171
+ // Check Python project has test command pointing to pytest
172
+ const hasPython = fs.existsSync(path.join(ROOT, 'backend', 'requirements.txt')) ||
173
+ fs.existsSync(path.join(ROOT, 'requirements.txt'));
174
+ if (hasPython && testMatch && !testMatch[1].includes('pytest')) {
175
+ addFinding('WARN', 'config',
176
+ 'Proyecto Python detectado pero test: no usa pytest',
177
+ `Cambia a: test: cd backend && py -3.13 -m pytest -x -v`);
178
+ }
179
+
180
+ // Check DESIGN_SYSTEM
181
+ const hasDesignSystem = fs.existsSync(path.join(ROOT, 'DESIGN_SYSTEM.md')) ||
182
+ fs.existsSync(path.join(ROOT, '.agentic', 'DESIGN_SYSTEM.md'));
183
+ if (!hasDesignSystem) {
184
+ addFinding('INFO', 'config', 'DESIGN_SYSTEM.md no encontrado',
185
+ 'Opcional: crea DESIGN_SYSTEM.md para que el agente Front tenga referencia de tokens');
186
+ }
187
+ }
188
+
189
+ // ── Check 4: Specs vs código ──────────────────────────────────────────────────
190
+
191
+ function checkSpecsVsCode() {
192
+ if (!fs.existsSync(SPECS_DIR)) {
193
+ addFinding('INFO', 'specs', 'No hay specs registradas aún', 'Se crean automáticamente durante ciclos aa:');
194
+ return;
195
+ }
196
+
197
+ const specFiles = fs.readdirSync(SPECS_DIR).filter(f => f.endsWith('.md'));
198
+ if (specFiles.length === 0) {
199
+ addFinding('INFO', 'specs', 'Directorio specs vacío', null);
200
+ return;
201
+ }
202
+
203
+ addFinding('INFO', 'specs', `${specFiles.length} specs encontradas`, null);
204
+
205
+ // Check specs for unresolved TODOs
206
+ for (const specFile of specFiles) {
207
+ const content = safeRead(path.join(SPECS_DIR, specFile)) || '';
208
+ const todos = (content.match(/\bTODO\b|\bPENDING\b|\bFIXME\b/gi) || []).length;
209
+ if (todos > 0) {
210
+ addFinding('WARN', 'specs',
211
+ `${specFile} tiene ${todos} TODOs/PENDINGs sin resolver`,
212
+ `Revisar y actualizar o eliminar entradas obsoletas`);
213
+ }
214
+ }
215
+ }
216
+
217
+ // ── Helpers ───────────────────────────────────────────────────────────────────
218
+
219
+ function findSourceFiles(root, extensions = ['.ts', '.tsx', '.js', '.py']) {
220
+ const results = [];
221
+ const skip = new Set(['node_modules', '.agentic', '.git', '__pycache__', '.next', 'dist', 'build']);
222
+
223
+ function walk(dir) {
224
+ try {
225
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
226
+ if (skip.has(entry.name)) continue;
227
+ const full = path.join(dir, entry.name);
228
+ if (entry.isDirectory()) walk(full);
229
+ else if (extensions.some(e => entry.name.endsWith(e))) results.push(full);
230
+ }
231
+ } catch {}
232
+ }
233
+
234
+ walk(root);
235
+ return results;
236
+ }
237
+
238
+ function safeRead(filePath) {
239
+ try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; }
240
+ }
241
+
242
+ // ── Report printer ────────────────────────────────────────────────────────────
243
+
244
+ function printReport() {
245
+ console.log('\n══════════════════════════════════════════════════');
246
+ console.log(' 🔍 Agentic KDD — Analyzer');
247
+ console.log('══════════════════════════════════════════════════');
248
+
249
+ const bySeverity = { HIGH: [], WARN: [], INFO: [] };
250
+ for (const f of findings) {
251
+ (bySeverity[f.severity] || bySeverity.INFO).push(f);
252
+ }
253
+
254
+ if (bySeverity.HIGH.length > 0) {
255
+ console.log('\n 🔴 HIGH — requieren atención:');
256
+ for (const f of bySeverity.HIGH) {
257
+ console.log(`\n [${f.category.toUpperCase()}] ${f.message}`);
258
+ if (f.suggestion) console.log(` → ${f.suggestion}`);
259
+ }
260
+ }
261
+
262
+ if (bySeverity.WARN.length > 0) {
263
+ console.log('\n 🟠 WARN — revisar:');
264
+ for (const f of bySeverity.WARN) {
265
+ console.log(`\n [${f.category.toUpperCase()}] ${f.message}`);
266
+ if (f.suggestion) console.log(` → ${f.suggestion}`);
267
+ }
268
+ }
269
+
270
+ if (bySeverity.INFO.length > 0) {
271
+ console.log('\n ✅ INFO:');
272
+ for (const f of bySeverity.INFO) {
273
+ console.log(` [${f.category.toUpperCase()}] ${f.message}`);
274
+ }
275
+ }
276
+
277
+ const total = findings.length;
278
+ const issues = bySeverity.HIGH.length + bySeverity.WARN.length;
279
+ console.log(`\n Total: ${total} checks | Problemas: ${issues}`);
280
+
281
+ if (issues === 0) {
282
+ console.log(' ✅ Todo consistente — el proyecto está en buen estado.');
283
+ } else if (bySeverity.HIGH.length > 0) {
284
+ console.log(' ⛔ Hay inconsistencias críticas — resolver antes del próximo ciclo aa:');
285
+ process.exit(1);
286
+ } else {
287
+ console.log(' ⚠️ Hay advertencias — revisar cuando sea posible.');
288
+ }
289
+
290
+ console.log('══════════════════════════════════════════════════\n');
291
+ }
292
+
293
+ // ── CLI ───────────────────────────────────────────────────────────────────────
294
+
295
+ if (require.main === module) {
296
+ const cmd = process.argv[2] || 'run';
297
+
298
+ if (cmd === 'run') {
299
+ checkContractsVsTests();
300
+ checkMemoryVsCode();
301
+ checkConfigVsStack();
302
+ checkSpecsVsCode();
303
+ } else if (cmd === 'contracts') {
304
+ checkContractsVsTests();
305
+ } else if (cmd === 'memory') {
306
+ checkMemoryVsCode();
307
+ } else if (cmd === 'spec') {
308
+ checkSpecsVsCode();
309
+ } else if (cmd === 'config') {
310
+ checkConfigVsStack();
311
+ } else {
312
+ console.log('Uso: node akdd-analyze.cjs [run|contracts|memory|spec|config]');
313
+ process.exit(0);
314
+ }
315
+
316
+ printReport();
317
+ }
318
+
319
+ module.exports = { checkContractsVsTests, checkMemoryVsCode, checkConfigVsStack, checkSpecsVsCode };
package/bin/akdd.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  const { init } = require('../src/init');
5
5
  const { update } = require('../src/update');
6
+ const { onboard } = require('../src/onboard');
6
7
  const { graph } = require('../src/graph');
7
8
  const { dashboard } = require('../src/dashboard');
8
9
  const { analyze } = require('../src/analyze');
@@ -24,6 +25,8 @@ const HELP = `
24
25
  Setup:
25
26
  akdd init Install Agentic KDD in the current project
26
27
  akdd update Update agents + engine (memory stays intact)
28
+ akdd onboard Analyze existing project + pre-populate memory
29
+ akdd analyze Cross-artifact consistency check
27
30
  akdd health System health check — what's configured, what's missing
28
31
  akdd health --fix Auto-fix common issues
29
32
 
@@ -155,6 +158,8 @@ switch (command) {
155
158
 
156
159
  case 'init': init(); break;
157
160
  case 'update': update(); break;
161
+ case 'onboard': onboard(); break;
162
+ case 'analyze': runModule('akdd-analyze.cjs', args[0] || 'run'); break;
158
163
  case 'analyze': analyze(); break;
159
164
 
160
165
  // ── v3.0: Health ──────────────────────────────────────────────────────