pumuki-ast-hooks 6.1.10 β†’ 6.1.13

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.
@@ -65,5 +65,65 @@ describe('Text Scanner Module', () => {
65
65
  const typeSafety = findings.find(f => f.ruleId === 'ios.optionals.type_safety');
66
66
  expect(typeSafety).toBeUndefined();
67
67
  });
68
+
69
+ it('flags switch-based message provider mapping to string literals', () => {
70
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ast-text-'));
71
+ const filePath = path.join(root, 'SupportMessageProvider.swift');
72
+ const content = [
73
+ 'enum SupportStatus {',
74
+ ' case active',
75
+ ' case inactive',
76
+ '}',
77
+ '',
78
+ 'struct SupportMessageProvider {',
79
+ ' func message(for status: SupportStatus) -> String {',
80
+ ' switch status {',
81
+ ' case .active:',
82
+ ' return "support.active"',
83
+ ' case .inactive:',
84
+ ' return "support.inactive"',
85
+ ' }',
86
+ ' }',
87
+ '}'
88
+ ].join('\n');
89
+
90
+ fs.writeFileSync(filePath, content, 'utf8');
91
+
92
+ const findings = [];
93
+ runTextScanner(root, findings);
94
+
95
+ const ocpFinding = findings.find(f => f.ruleId === 'ios.solid.ocp_switch_to_string');
96
+ expect(ocpFinding).toBeDefined();
97
+ });
98
+ });
99
+
100
+ describe('android rules', () => {
101
+ it('flags when-based message provider mapping to string literals', () => {
102
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ast-text-'));
103
+ const filePath = path.join(root, 'SupportMessageProvider.kt');
104
+ const content = [
105
+ 'enum class SupportStatus {',
106
+ ' ACTIVE,',
107
+ ' INACTIVE',
108
+ '}',
109
+ '',
110
+ 'class SupportMessageProvider {',
111
+ ' fun message(status: SupportStatus): String {',
112
+ ' return when (status) {',
113
+ ' SupportStatus.ACTIVE -> "support.active"',
114
+ ' SupportStatus.INACTIVE -> "support.inactive"',
115
+ ' }',
116
+ ' }',
117
+ '}'
118
+ ].join('\n');
119
+
120
+ fs.writeFileSync(filePath, content, 'utf8');
121
+
122
+ const findings = [];
123
+ runTextScanner(root, findings);
124
+
125
+ const ocpFinding = findings.find(f => f.ruleId === 'android.solid.ocp_switch_to_string');
126
+ expect(ocpFinding).toBeDefined();
127
+ });
68
128
  });
69
129
  });
@@ -15,6 +15,58 @@ function walk(dir, acc) {
15
15
  }
16
16
  }
17
17
 
18
+ function hasProviderNameHint(content, filePath) {
19
+ const providerNamePattern = /\b(class|struct|object)\s+[A-Za-z_][A-Za-z0-9_]*(Message|Notice|Localization|Provider|Presenter|Mapper)[A-Za-z0-9_]*\b/i;
20
+ if (providerNamePattern.test(content)) return true;
21
+ const baseName = path.basename(filePath, path.extname(filePath));
22
+ return /Message|Notice|Localization|Provider|Presenter|Mapper/i.test(baseName);
23
+ }
24
+
25
+ function hasSwiftMappingHint(content) {
26
+ return /\brawValue\b/.test(content) || /\bDictionary<[^>]*String\b/.test(content) || /\[[A-Za-z_][A-Za-z0-9_]*\s*:\s*String\]/.test(content);
27
+ }
28
+
29
+ function hasKotlinMappingHint(content) {
30
+ return /\bmapOf\s*\(|\bmutableMapOf\s*\(|\bMap<[^>]*String\b/.test(content);
31
+ }
32
+
33
+ function hasSwiftSwitchToString(content) {
34
+ const switchPattern = /switch\s+[A-Za-z_][A-Za-z0-9_]*\s*\{[^}]*\}/g;
35
+ let match;
36
+ while ((match = switchPattern.exec(content)) !== null) {
37
+ const block = match[0];
38
+ if (!/\bcase\b/.test(block)) continue;
39
+ if (!/\"[^\"]+\"/.test(block)) continue;
40
+ if (!/case\s+(\.|[A-Za-z_][A-Za-z0-9_]*\.)[A-Za-z_][A-Za-z0-9_]*\s*:/.test(block)) continue;
41
+ if (/\breturn\b/.test(block) || /case\s+[^\n:]+:\s*\"[^\"]+\"/.test(block)) return true;
42
+ }
43
+ return false;
44
+ }
45
+
46
+ function hasKotlinWhenToString(content) {
47
+ const whenPattern = /when\s*\([^)]*\)\s*\{[^}]*\}/g;
48
+ let match;
49
+ while ((match = whenPattern.exec(content)) !== null) {
50
+ const block = match[0];
51
+ if (!/[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\s*->\s*\"[^\"]+\"/.test(block)) continue;
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ function shouldFlagMessageProviderSwitch(content, filePath, language) {
58
+ if (!hasProviderNameHint(content, filePath)) return false;
59
+ if (language === 'swift') {
60
+ if (hasSwiftMappingHint(content)) return false;
61
+ return hasSwiftSwitchToString(content);
62
+ }
63
+ if (language === 'kotlin') {
64
+ if (hasKotlinMappingHint(content)) return false;
65
+ return hasKotlinWhenToString(content);
66
+ }
67
+ return false;
68
+ }
69
+
18
70
  function runTextScanner(root, findings) {
19
71
  const files = [];
20
72
  let iosAnchorFile = null;
@@ -541,6 +593,11 @@ function runTextScanner(root, findings) {
541
593
  if (/Handler\(\)\.post/.test(content) && !/@SuppressLint\("HandlerLeak"\)/.test(content)) {
542
594
  pushFileFinding('android.antipattern.handler_leak', 'high', file, 1, 1, 'Handler without weak reference - potential memory leak', findings);
543
595
  }
596
+ const isAnalyzer = /infrastructure\/ast\/|analyzers\/|detectors\/|scanner|analyzer|detector/i.test(file);
597
+ const isTestFile = /\.(spec|test)\.(js|ts|kt|kts)$/i.test(file);
598
+ if ((ext === '.kt' || ext === '.kts') && !isAnalyzer && !isTestFile && shouldFlagMessageProviderSwitch(content, file, 'kotlin')) {
599
+ pushFileFinding('android.solid.ocp_switch_to_string', 'medium', file, 1, 1, 'OCP warning: switch-to-string mapping in message/localization provider - prefer injected map or enum raw values', findings);
600
+ }
544
601
  }
545
602
 
546
603
  if (plat === 'ios') {
@@ -636,6 +693,9 @@ function runTextScanner(root, findings) {
636
693
  if (/Text\(\s*\"[^\"]+\"\s*\)/.test(content) && !/NSLocalizedString|\.localized/.test(content)) {
637
694
  pushFileFinding('ios.i18n.hardcoded_strings', 'medium', file, 1, 1, 'Hardcoded string in Text() without localization', findings);
638
695
  }
696
+ if (!isAnalyzer && !isTestFile && shouldFlagMessageProviderSwitch(content, file, 'swift')) {
697
+ pushFileFinding('ios.solid.ocp_switch_to_string', 'medium', file, 1, 1, 'OCP warning: switch-to-string mapping in message/localization provider - prefer RawRepresentable or injected map', findings);
698
+ }
639
699
  if (/Date\(/.test(content) && !/DateFormatter/.test(content)) {
640
700
  pushFileFinding('ios.i18n.missing_date_formatter', 'medium', file, 1, 1, 'Date rendered without DateFormatter', findings);
641
701
  }
@@ -129,6 +129,46 @@ describe('intelligent-audit', () => {
129
129
  expect(updated.timestamp).toBeDefined();
130
130
  expect(updated.timestamp).not.toBe(previous.timestamp);
131
131
  });
132
+
133
+ it('should include medium and low violations in ai_gate output when critical or high exist', async () => {
134
+ const evidencePath = path.join(process.cwd(), '.AI_EVIDENCE.json');
135
+ const original = fs.existsSync(evidencePath) ? fs.readFileSync(evidencePath, 'utf8') : null;
136
+ const minimal = {
137
+ timestamp: new Date().toISOString(),
138
+ severity_metrics: {
139
+ last_updated: new Date().toISOString(),
140
+ total_violations: 0,
141
+ by_severity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
142
+ },
143
+ ai_gate: { status: 'ALLOWED', scope: 'staging', last_check: new Date().toISOString(), violations: [], instruction: 'x', mandatory: true }
144
+ };
145
+
146
+ try {
147
+ fs.writeFileSync(evidencePath, JSON.stringify(minimal, null, 2));
148
+
149
+ const mod = require('../intelligent-audit');
150
+ const violations = [
151
+ { severity: 'CRITICAL', ruleId: 'rule.critical', filePath: 'apps/a.ts', line: 1, message: 'c' },
152
+ { severity: 'HIGH', ruleId: 'rule.high', filePath: 'apps/b.ts', line: 2, message: 'h' },
153
+ { severity: 'MEDIUM', ruleId: 'rule.medium', filePath: 'apps/c.ts', line: 3, message: 'm' },
154
+ { severity: 'LOW', ruleId: 'rule.low', filePath: 'apps/d.ts', line: 4, message: 'l' }
155
+ ];
156
+
157
+ await mod.updateAIEvidence(violations, { passed: false, blockedBy: 'critical' }, { estimated: 1, percentUsed: 0, remaining: 1 });
158
+
159
+ const updated = JSON.parse(fs.readFileSync(evidencePath, 'utf8'));
160
+ const severities = updated.ai_gate.violations.map(v => v.severity);
161
+ expect(severities).toEqual(expect.arrayContaining(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']));
162
+ } finally {
163
+ if (original === null) {
164
+ if (fs.existsSync(evidencePath)) {
165
+ fs.unlinkSync(evidencePath);
166
+ }
167
+ } else {
168
+ fs.writeFileSync(evidencePath, original);
169
+ }
170
+ }
171
+ });
132
172
  });
133
173
 
134
174
  describe('AI_EVIDENCE.json structure validation', () => {
@@ -736,11 +736,10 @@ async function updateAIEvidence(violations, gateResult, tokenUsage) {
736
736
  const mediumViolations = violations.filter(v => v.severity === 'MEDIUM');
737
737
  const lowViolations = violations.filter(v => v.severity === 'LOW');
738
738
 
739
- let gateViolations = [...criticalViolations, ...highViolations];
740
- if (gateViolations.length === 0) {
741
- gateViolations = [...mediumViolations, ...lowViolations];
742
- }
743
- const blockingViolations = gateViolations.slice(0, 50);
739
+ const maxGateViolations = env.getNumber('AI_GATE_MAX_VIOLATIONS', 200);
740
+ const gateViolationsLimit = Number.isFinite(maxGateViolations) && maxGateViolations > 0 ? maxGateViolations : 200;
741
+ const gateViolations = [...criticalViolations, ...highViolations, ...mediumViolations, ...lowViolations];
742
+ const blockingViolations = gateViolations.slice(0, gateViolationsLimit);
744
743
 
745
744
  const gateScope = String(env.get('AI_GATE_SCOPE', 'staging') || 'staging').trim().toLowerCase();
746
745
 
@@ -0,0 +1,17 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ describe('gitflow-enforcer', () => {
5
+ const scriptPath = path.join(__dirname, '..', 'gitflow-enforcer.sh');
6
+
7
+ test('debe ignorar xcuserdata en atomicidad', () => {
8
+ const script = fs.readFileSync(scriptPath, 'utf8');
9
+ expect(script).toMatch(/xcuserdata/);
10
+ });
11
+
12
+ test('debe omitir lint:hooks si no esta configurado', () => {
13
+ const script = fs.readFileSync(scriptPath, 'utf8');
14
+ expect(script).toMatch(/lint:hooks/);
15
+ expect(script).toMatch(/no configurado/);
16
+ });
17
+ });
@@ -93,11 +93,24 @@ refresh_evidence() {
93
93
  printf "${GREEN}βœ… Evidencia renovada (${reason}).${NC}\n"
94
94
  }
95
95
 
96
+ has_lint_hooks_script() {
97
+ local pkg="$1"
98
+ if [[ ! -f "$pkg" ]]; then
99
+ return 1
100
+ fi
101
+ if node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));process.exit(p&&p.scripts&&p.scripts['lint:hooks']?0:1);" "$pkg" >/dev/null 2>&1; then
102
+ return 0
103
+ fi
104
+ return 1
105
+ }
106
+
96
107
  lint_hooks_system() {
97
108
  local repo_pkg="${REPO_ROOT}/package.json"
98
109
  local hooks_pkg="${REPO_ROOT}/scripts/hooks-system/package.json"
110
+ local ran=0
99
111
 
100
- if [[ -f "${repo_pkg}" ]]; then
112
+ if has_lint_hooks_script "${repo_pkg}"; then
113
+ ran=1
101
114
  printf "${CYAN}πŸ”Ž Ejecutando lint (repo root)...${NC}\n"
102
115
  if npm --prefix "${REPO_ROOT}" run lint:hooks; then
103
116
  printf "${GREEN}βœ… Lint hooks-system OK.${NC}\n"
@@ -105,7 +118,8 @@ lint_hooks_system() {
105
118
  fi
106
119
  fi
107
120
 
108
- if [[ -f "${hooks_pkg}" ]]; then
121
+ if has_lint_hooks_script "${hooks_pkg}"; then
122
+ ran=1
109
123
  printf "${CYAN}πŸ”Ž Ejecutando lint (scripts/hooks-system)...${NC}\n"
110
124
  if npm --prefix "${REPO_ROOT}/scripts/hooks-system" run lint:hooks; then
111
125
  printf "${GREEN}βœ… Lint hooks-system OK.${NC}\n"
@@ -113,6 +127,11 @@ lint_hooks_system() {
113
127
  fi
114
128
  fi
115
129
 
130
+ if [[ "$ran" -eq 0 ]]; then
131
+ printf "${YELLOW}ℹ️ lint:hooks no configurado; se omite.${NC}\n"
132
+ return 0
133
+ fi
134
+
116
135
  printf "${RED}❌ Lint hooks-system fallΓ³.${NC}\n"
117
136
  return 1
118
137
  }
@@ -178,12 +197,17 @@ verify_atomic_commit() {
178
197
  local roots_list=""
179
198
  local root_count=0
180
199
  for file in "${files[@]}"; do
200
+ case "$file" in
201
+ */xcuserdata/*|*/xcuserdatad/*)
202
+ continue
203
+ ;;
204
+ esac
181
205
  local root="${file%%/*}"
182
206
  if [[ "$root" == "$file" ]]; then
183
207
  root="(root)"
184
208
  fi
185
209
  case "$root" in
186
- .AI_EVIDENCE.json|README.md|CHANGELOG.md|docs )
210
+ .AI_EVIDENCE.json|README.md|CHANGELOG.md|docs|.audit_tmp|.audit-reports|.cursor|.windsurf|xcuserdata|xcuserdatad )
187
211
  continue
188
212
  ;;
189
213
  "" )
@@ -127,3 +127,24 @@
127
127
  {"timestamp":"2026-01-15T14:53:25.770Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
128
128
  {"timestamp":"2026-01-15T14:53:25.772Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
129
129
  {"timestamp":"2026-01-15T14:53:25.772Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
130
+ {"timestamp":"2026-01-21T12:12:08.680Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
131
+ {"timestamp":"2026-01-21T12:12:08.681Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
132
+ {"timestamp":"2026-01-21T12:12:08.681Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
133
+ {"timestamp":"2026-01-21T13:01:16.348Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
134
+ {"timestamp":"2026-01-21T13:01:16.349Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
135
+ {"timestamp":"2026-01-21T13:01:16.350Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
136
+ {"timestamp":"2026-01-21T13:17:08.086Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
137
+ {"timestamp":"2026-01-21T13:17:08.088Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
138
+ {"timestamp":"2026-01-21T13:17:08.089Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
139
+ {"timestamp":"2026-01-21T14:08:15.939Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
140
+ {"timestamp":"2026-01-21T14:08:15.943Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
141
+ {"timestamp":"2026-01-21T14:08:15.945Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
142
+ {"timestamp":"2026-01-21T14:14:45.047Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
143
+ {"timestamp":"2026-01-21T14:14:45.048Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
144
+ {"timestamp":"2026-01-21T14:14:45.048Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
145
+ {"timestamp":"2026-01-21T14:19:16.416Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
146
+ {"timestamp":"2026-01-21T14:19:16.417Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
147
+ {"timestamp":"2026-01-21T14:19:16.418Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
148
+ {"timestamp":"2026-01-21T14:43:17.747Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
149
+ {"timestamp":"2026-01-21T14:43:17.748Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
150
+ {"timestamp":"2026-01-21T14:43:17.749Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}