@nforma.ai/nforma 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* telemetry-collector.cjs
|
|
6
|
+
*
|
|
7
|
+
* Pure disk I/O telemetry collector for QGSD.
|
|
8
|
+
* Reads existing log sources, aggregates stats, writes .planning/telemetry/report.json.
|
|
9
|
+
*
|
|
10
|
+
* Sources:
|
|
11
|
+
* 1. ~/.claude/debug/*.txt — MCP tool call timing and failures
|
|
12
|
+
* 2. .planning/quorum-scoreboard.json — quorum availability stats
|
|
13
|
+
* 3. .claude/circuit-breaker-state.json — circuit breaker state
|
|
14
|
+
*
|
|
15
|
+
* NEVER spawns Claude or calls any MCP tool. Handles all missing files gracefully.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node bin/telemetry-collector.cjs
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
const DEBUG_DIR = path.join(os.homedir(), '.claude', 'debug');
|
|
26
|
+
const MAX_FILES = 100;
|
|
27
|
+
const MAX_DAYS = 7;
|
|
28
|
+
const PROJECT_DIR = process.cwd();
|
|
29
|
+
const TELEMETRY_DIR = path.join(PROJECT_DIR, '.planning', 'telemetry');
|
|
30
|
+
|
|
31
|
+
// ─── Regex patterns (reused from review-mcp-logs.cjs) ────────────────────────
|
|
32
|
+
const RE_COMPLETE = /MCP server "([^"]+)": Tool '([^']+)' completed successfully in (\d+)ms/;
|
|
33
|
+
const RE_FAILED = /MCP server "([^"]+)": Tool '([^']+)' failed after (\d+)s: (.+)/;
|
|
34
|
+
|
|
35
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
36
|
+
function percentile(arr, p) {
|
|
37
|
+
if (!arr.length) return 0;
|
|
38
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
39
|
+
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
|
40
|
+
return sorted[Math.max(0, idx)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Source 1: MCP debug logs ─────────────────────────────────────────────────
|
|
44
|
+
const servers = {}; // serverName -> { totalCalls, successCount, failureCount, durations[], errors[] }
|
|
45
|
+
|
|
46
|
+
function ensureServer(name) {
|
|
47
|
+
if (!servers[name]) {
|
|
48
|
+
servers[name] = { totalCalls: 0, successCount: 0, failureCount: 0, durations: [], errors: [] };
|
|
49
|
+
}
|
|
50
|
+
return servers[name];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let mcpResult = {
|
|
54
|
+
servers: {},
|
|
55
|
+
alwaysFailing: [],
|
|
56
|
+
slowServers: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const cutoff = Date.now() - MAX_DAYS * 86400 * 1000;
|
|
61
|
+
|
|
62
|
+
let files = [];
|
|
63
|
+
if (fs.existsSync(DEBUG_DIR)) {
|
|
64
|
+
files = fs.readdirSync(DEBUG_DIR)
|
|
65
|
+
.filter(f => f.endsWith('.txt'))
|
|
66
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(DEBUG_DIR, f)).mtimeMs }))
|
|
67
|
+
.filter(f => f.mtime >= cutoff)
|
|
68
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
69
|
+
.slice(0, MAX_FILES)
|
|
70
|
+
.map(f => path.join(DEBUG_DIR, f.name));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
let content;
|
|
75
|
+
try { content = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
76
|
+
|
|
77
|
+
for (const line of content.split('\n')) {
|
|
78
|
+
let m;
|
|
79
|
+
|
|
80
|
+
if ((m = line.match(RE_COMPLETE))) {
|
|
81
|
+
const [, server, , ms] = m;
|
|
82
|
+
const s = ensureServer(server);
|
|
83
|
+
const durationMs = parseInt(ms, 10);
|
|
84
|
+
s.totalCalls++;
|
|
85
|
+
s.successCount++;
|
|
86
|
+
s.durations.push(durationMs);
|
|
87
|
+
// hang if successful call > 60000ms
|
|
88
|
+
if (durationMs > 60000) {
|
|
89
|
+
// count as hang but still a success
|
|
90
|
+
}
|
|
91
|
+
} else if ((m = line.match(RE_FAILED))) {
|
|
92
|
+
const [, server, , sec, reason] = m;
|
|
93
|
+
const s = ensureServer(server);
|
|
94
|
+
const durationMs = parseInt(sec, 10) * 1000;
|
|
95
|
+
s.totalCalls++;
|
|
96
|
+
s.failureCount++;
|
|
97
|
+
s.errors.push(reason.trim().slice(0, 120));
|
|
98
|
+
if (durationMs > 60000) {
|
|
99
|
+
// hang: counted separately
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Build per-server stats
|
|
106
|
+
for (const [name, data] of Object.entries(servers)) {
|
|
107
|
+
// Count hangs: completed calls with duration > 60000ms
|
|
108
|
+
// We stored durations only for successful calls; failed calls with >60s are also hangs
|
|
109
|
+
const hangCount = data.durations.filter(d => d > 60000).length;
|
|
110
|
+
|
|
111
|
+
// Top 3 error reasons by frequency
|
|
112
|
+
const errFreq = {};
|
|
113
|
+
for (const e of data.errors) {
|
|
114
|
+
errFreq[e] = (errFreq[e] || 0) + 1;
|
|
115
|
+
}
|
|
116
|
+
const topErrors = Object.entries(errFreq)
|
|
117
|
+
.sort(([, a], [, b]) => b - a)
|
|
118
|
+
.slice(0, 3)
|
|
119
|
+
.map(([msg]) => msg);
|
|
120
|
+
|
|
121
|
+
const p95Ms = percentile(data.durations, 95);
|
|
122
|
+
|
|
123
|
+
mcpResult.servers[name] = {
|
|
124
|
+
totalCalls: data.totalCalls,
|
|
125
|
+
failureCount: data.failureCount,
|
|
126
|
+
hangCount,
|
|
127
|
+
topErrors,
|
|
128
|
+
p95Ms,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// alwaysFailing: servers with totalCalls > 0 and successCount === 0
|
|
133
|
+
mcpResult.alwaysFailing = Object.entries(servers)
|
|
134
|
+
.filter(([, s]) => s.totalCalls > 0 && s.successCount === 0)
|
|
135
|
+
.map(([name]) => name);
|
|
136
|
+
|
|
137
|
+
// slowServers: p95 > 30000ms and at least 1 success
|
|
138
|
+
mcpResult.slowServers = Object.entries(servers)
|
|
139
|
+
.filter(([, s]) => {
|
|
140
|
+
const p95 = percentile(s.durations, 95);
|
|
141
|
+
return p95 > 30000 && s.successCount > 0;
|
|
142
|
+
})
|
|
143
|
+
.map(([name, s]) => ({ name, p95Ms: percentile(s.durations, 95) }))
|
|
144
|
+
.sort((a, b) => b.p95Ms - a.p95Ms);
|
|
145
|
+
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Non-fatal: leave mcpResult with empty defaults
|
|
148
|
+
process.stderr.write('[telemetry-collector] MCP parse error: ' + err.message + '\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Source 2: quorum-scoreboard.json ─────────────────────────────────────────
|
|
152
|
+
let quorumResult = {
|
|
153
|
+
totalRounds: 0,
|
|
154
|
+
allUnavailableRounds: 0,
|
|
155
|
+
quorumFailureRate: 0,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const pp = require('./planning-paths.cjs');
|
|
160
|
+
const scoreboardPath = pp.resolveWithFallback(PROJECT_DIR, 'quorum-scoreboard');
|
|
161
|
+
if (fs.existsSync(scoreboardPath)) {
|
|
162
|
+
const sb = JSON.parse(fs.readFileSync(scoreboardPath, 'utf8'));
|
|
163
|
+
if (Array.isArray(sb.rounds)) {
|
|
164
|
+
const total = sb.rounds.length;
|
|
165
|
+
let allUnavail = 0;
|
|
166
|
+
for (const round of sb.rounds) {
|
|
167
|
+
// A round is "all unavailable" if votes is empty or every model is UNAVAILABLE
|
|
168
|
+
if (!round.votes || Object.keys(round.votes).length === 0) {
|
|
169
|
+
allUnavail++;
|
|
170
|
+
} else {
|
|
171
|
+
const vals = Object.values(round.votes);
|
|
172
|
+
if (vals.length > 0 && vals.every(v => v === 'UNAVAILABLE')) {
|
|
173
|
+
allUnavail++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
quorumResult.totalRounds = total;
|
|
178
|
+
quorumResult.allUnavailableRounds = allUnavail;
|
|
179
|
+
quorumResult.quorumFailureRate = total > 0 ? allUnavail / total : 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
process.stderr.write('[telemetry-collector] Scoreboard parse error: ' + err.message + '\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Source 3: circuit-breaker-state.json ─────────────────────────────────────
|
|
187
|
+
let cbResult = {
|
|
188
|
+
active: false,
|
|
189
|
+
triggerCount: 0,
|
|
190
|
+
lastTriggeredAt: null,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const cbPath = path.join(PROJECT_DIR, '.claude', 'circuit-breaker-state.json');
|
|
195
|
+
if (fs.existsSync(cbPath)) {
|
|
196
|
+
const cb = JSON.parse(fs.readFileSync(cbPath, 'utf8'));
|
|
197
|
+
cbResult.active = Boolean(cb.active);
|
|
198
|
+
cbResult.triggerCount = typeof cb.triggerCount === 'number' ? cb.triggerCount : 0;
|
|
199
|
+
cbResult.lastTriggeredAt = cb.lastTriggeredAt || null;
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
process.stderr.write('[telemetry-collector] Circuit breaker parse error: ' + err.message + '\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Build report ─────────────────────────────────────────────────────────────
|
|
206
|
+
const report = {
|
|
207
|
+
generatedAt: new Date().toISOString(),
|
|
208
|
+
mcp: {
|
|
209
|
+
servers: mcpResult.servers,
|
|
210
|
+
alwaysFailing: mcpResult.alwaysFailing,
|
|
211
|
+
slowServers: mcpResult.slowServers,
|
|
212
|
+
},
|
|
213
|
+
quorum: {
|
|
214
|
+
totalRounds: quorumResult.totalRounds,
|
|
215
|
+
allUnavailableRounds: quorumResult.allUnavailableRounds,
|
|
216
|
+
quorumFailureRate: quorumResult.quorumFailureRate,
|
|
217
|
+
},
|
|
218
|
+
circuitBreaker: {
|
|
219
|
+
active: cbResult.active,
|
|
220
|
+
triggerCount: cbResult.triggerCount,
|
|
221
|
+
lastTriggeredAt: cbResult.lastTriggeredAt,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// ─── Write output ─────────────────────────────────────────────────────────────
|
|
226
|
+
fs.mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
227
|
+
const reportPath = path.join(TELEMETRY_DIR, 'report.json');
|
|
228
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
|
229
|
+
console.log('[telemetry-collector] Report written to ' + reportPath);
|