@monoes/monomindcli 1.10.10 → 1.10.11

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.
@@ -157,6 +157,122 @@ function _recordGraphTelemetry(event) {
157
157
  } catch (e) { /* non-fatal */ }
158
158
  }
159
159
 
160
+ // ── Loop drift detection ───────────────────────────────────────────────────────
161
+ // Record tool call signatures per session, warn when the same call recurs ≥3×.
162
+ function _recordToolCall(signature) {
163
+ try {
164
+ var f = path.join(CWD, '.monomind', 'metrics', 'tool-calls.json');
165
+ fs.mkdirSync(path.dirname(f), { recursive: true });
166
+ var d = {};
167
+ try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
168
+ if (typeof d !== 'object' || d === null) d = {};
169
+ // Roll over every 4 hours (new session)
170
+ if (!d.startedAt || (Date.now() - d.startedAt) > 4 * 60 * 60 * 1000) {
171
+ d = { startedAt: Date.now(), calls: {} };
172
+ }
173
+ d.calls[signature] = (d.calls[signature] || 0) + 1;
174
+ fs.writeFileSync(f, JSON.stringify(d));
175
+ return d.calls[signature];
176
+ } catch (e) { return 0; }
177
+ }
178
+
179
+ // ── Cost budget ────────────────────────────────────────────────────────────────
180
+ // Read today's cost from token-summary and compare against budget ceiling.
181
+ function _getBudgetStatus() {
182
+ try {
183
+ var budgetFile = path.join(CWD, '.monomind', 'budget.json');
184
+ var summaryFile = path.join(CWD, '.monomind', 'metrics', 'token-summary.json');
185
+ if (!fs.existsSync(summaryFile)) return null;
186
+ var summary = JSON.parse(fs.readFileSync(summaryFile, 'utf-8'));
187
+ // Support both shapes: { todayCost, monthCost } and { today: { cost }, month: { cost } }
188
+ var todayCost = summary.todayCost || (summary.today && summary.today.cost) || 0;
189
+ var monthCost = summary.monthCost || (summary.month && summary.month.cost) || 0;
190
+ var dailyLimit = 50, monthlyLimit = 1500; // defaults
191
+ try {
192
+ if (fs.existsSync(budgetFile)) {
193
+ var b = JSON.parse(fs.readFileSync(budgetFile, 'utf-8'));
194
+ dailyLimit = b.dailyLimit || dailyLimit;
195
+ monthlyLimit = b.monthlyLimit || monthlyLimit;
196
+ }
197
+ } catch (_) {}
198
+ var dailyPct = Math.round((todayCost / dailyLimit) * 100);
199
+ var monthlyPct = Math.round((monthCost / monthlyLimit) * 100);
200
+ return {
201
+ todayCost: todayCost, monthCost: monthCost,
202
+ dailyLimit: dailyLimit, monthlyLimit: monthlyLimit,
203
+ dailyPct: dailyPct, monthlyPct: monthlyPct,
204
+ alert: dailyPct >= 80 || monthlyPct >= 80,
205
+ breached: dailyPct >= 100 || monthlyPct >= 100,
206
+ };
207
+ } catch (e) { return null; }
208
+ }
209
+
210
+ // ── Test feedback (detection only — do not auto-run) ──────────────────────────
211
+ // When LLM edits a source file, find tests that import it via monograph and
212
+ // surface the list so the LLM (or user) knows what to verify.
213
+ function _findAffectedTests(filePath) {
214
+ if (!filePath) return [];
215
+ var db = _openMonographDb();
216
+ if (!db) return [];
217
+ try {
218
+ var rel = filePath;
219
+ if (filePath.indexOf(CWD) === 0) rel = filePath.slice(CWD.length + 1);
220
+ // Find tests that IMPORTS any symbol whose target file_path matches our file.
221
+ // The graph stores IMPORTS edges to symbol nodes, not file nodes — so we
222
+ // match on the target node's file_path field instead of target_id directly.
223
+ var rows = db.prepare(
224
+ "SELECT DISTINCT src.file_path FROM edges e " +
225
+ "JOIN nodes src ON e.source_id = src.id " +
226
+ "JOIN nodes tgt ON e.target_id = tgt.id " +
227
+ "WHERE e.relation IN ('IMPORTS','CALLS','DEPENDS_ON') " +
228
+ "AND (tgt.file_path = ? OR tgt.file_path = ?) " +
229
+ "AND src.file_path IS NOT NULL AND src.file_path != '' " +
230
+ "AND (src.file_path LIKE '%test%' OR src.file_path LIKE '%.spec.%' OR src.file_path LIKE '%__tests__%') " +
231
+ "AND src.file_path NOT LIKE '%.worktrees%' " +
232
+ "LIMIT 5"
233
+ ).all(filePath, rel);
234
+ return rows.map(function(r) { return r.file_path; });
235
+ } catch (e) { return []; }
236
+ finally { try { db.close(); } catch (_) {} }
237
+ }
238
+
239
+ // ── Hook latency tracking ─────────────────────────────────────────────────────
240
+ function _recordHookLatency(handlerName, durationMs) {
241
+ try {
242
+ var f = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
243
+ fs.mkdirSync(path.dirname(f), { recursive: true });
244
+ var d = {};
245
+ try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
246
+ if (typeof d !== 'object' || d === null) d = {};
247
+ var entry = d[handlerName] || { count: 0, total: 0, max: 0 };
248
+ entry.count++;
249
+ entry.total += durationMs;
250
+ entry.max = Math.max(entry.max, durationMs);
251
+ entry.mean = Math.round(entry.total / entry.count);
252
+ d[handlerName] = entry;
253
+ d.lastUpdated = Date.now();
254
+ fs.writeFileSync(f, JSON.stringify(d));
255
+ } catch (e) {}
256
+ }
257
+
258
+ // ── Auto-ADR decision detection ───────────────────────────────────────────────
259
+ // Record sentence-level decision markers from user prompts to .monomind/decisions.jsonl
260
+ function _recordDecisionMarkers(promptText) {
261
+ if (!promptText || typeof promptText !== 'string') return;
262
+ var markers = /\b(let's go with|we (?:chose|decided|picked|will go with)|decision[:\s]|choosing|going with|prefer to|let's use)\b[^\.\n]{0,200}/gi;
263
+ var matches = promptText.match(markers);
264
+ if (!matches || matches.length === 0) return;
265
+ try {
266
+ var f = path.join(CWD, '.monomind', 'decisions.jsonl');
267
+ var entry = JSON.stringify({
268
+ ts: Date.now(),
269
+ excerpts: matches.slice(0, 3),
270
+ prompt: promptText.slice(0, 400),
271
+ });
272
+ fs.appendFileSync(f, entry + '\n');
273
+ } catch (e) {}
274
+ }
275
+
160
276
  // Auto-rebuild the monograph after N writes — graph staleness during heavy
161
277
  // editing is the main reason suggestions go cold. Triggered by post-edit.
162
278
  function _maybeRebuildMonograph() {
@@ -836,6 +952,21 @@ const handlers = {
836
952
 
837
953
  console.log(output.join('\n'));
838
954
 
955
+ // Record any decision markers in this prompt (auto-ADR pipeline).
956
+ try { _recordDecisionMarkers(prompt); } catch (e) {}
957
+
958
+ // Cost budget — emit amber/red banner when approaching limit.
959
+ try {
960
+ var budget = _getBudgetStatus();
961
+ if (budget && budget.alert) {
962
+ if (budget.breached) {
963
+ console.log('[BUDGET_BREACHED] Daily $' + budget.todayCost.toFixed(2) + '/$' + budget.dailyLimit + ' (' + budget.dailyPct + '%) · Monthly $' + budget.monthCost.toFixed(2) + '/$' + budget.monthlyLimit + ' (' + budget.monthlyPct + '%). Switch to Haiku with /model haiku or raise limits in .monomind/budget.json.');
964
+ } else {
965
+ console.log('[BUDGET_ALERT] Daily ' + budget.dailyPct + '% of $' + budget.dailyLimit + ' · Monthly ' + budget.monthlyPct + '% of $' + budget.monthlyLimit + '. Adjust .monomind/budget.json if needed.');
966
+ }
967
+ }
968
+ } catch (e) {}
969
+
839
970
  // Inject monograph hint for complex tasks.
840
971
  // Source of truth is .monomind/monograph.db (SQLite). Legacy stats.json
841
972
  // is no longer written by the build, so it is checked only as a fallback.
@@ -987,6 +1118,13 @@ const handlers = {
987
1118
  pattern = findMatch[1] || findMatch[2] || findMatch[3] || '';
988
1119
  _recordGraphTelemetry('bash_find_call');
989
1120
  }
1121
+ if (pattern && pattern.length >= 3) {
1122
+ var sigB = 'Bash-grep:' + pattern.slice(0, 60);
1123
+ var countB = _recordToolCall(sigB);
1124
+ if (countB >= 3) {
1125
+ console.log('[LOOP_DRIFT] You\'ve grepped "' + pattern.slice(0, 40) + '" ' + countB + 'x — switch to monograph_query.');
1126
+ }
1127
+ }
990
1128
  if (pattern && pattern.length >= 3) {
991
1129
  var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
992
1130
  if (clean.length >= 3) {
@@ -1015,6 +1153,13 @@ const handlers = {
1015
1153
  if (typeof pattern !== 'string' || pattern.length < 3) return;
1016
1154
  var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
1017
1155
  if (clean.length < 3) return;
1156
+
1157
+ // Loop drift detection
1158
+ var sig = (toolName || 'Search') + ':' + clean.slice(0, 60);
1159
+ var count = _recordToolCall(sig);
1160
+ if (count >= 3) {
1161
+ console.log('[LOOP_DRIFT] You\'ve searched "' + clean.slice(0, 40) + '" ' + count + 'x this session — try monograph_query or monograph_impact for a structural answer.');
1162
+ }
1018
1163
  var suggestions = getMonographSuggestions(clean, 5);
1019
1164
  if (suggestions.length === 0) return;
1020
1165
  console.log('[MONOGRAPH_HIT] Graph already knows ' + suggestions.length + ' file(s) matching "' + clean.slice(0, 40) + '":');
@@ -1064,6 +1209,22 @@ const handlers = {
1064
1209
  }
1065
1210
  // Increment write counter and rebuild monograph when threshold hit.
1066
1211
  _maybeRebuildMonograph();
1212
+
1213
+ // Test feedback (detection-only): when editing a source file, list tests
1214
+ // that import it so the LLM/user knows what to verify next.
1215
+ try {
1216
+ var editedFile = hookInput.file_path || toolInput.file_path
1217
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
1218
+ if (editedFile && !editedFile.match(/\.(test|spec)\./) && !editedFile.includes('__tests__')) {
1219
+ var affectedTests = _findAffectedTests(editedFile);
1220
+ if (affectedTests.length > 0) {
1221
+ console.log('[AFFECTED_TESTS] ' + affectedTests.length + ' test(s) cover this file:');
1222
+ for (var ti = 0; ti < Math.min(5, affectedTests.length); ti++) {
1223
+ console.log(' · ' + affectedTests[ti]);
1224
+ }
1225
+ }
1226
+ }
1227
+ } catch (e) {}
1067
1228
  // ── Security-Sensitive File Auto-Alert ────────────────────────────────
1068
1229
  // When editing auth, security, crypto, or env-related files, flag it
1069
1230
  try {
@@ -2052,15 +2213,18 @@ const handlers = {
2052
2213
  };
2053
2214
 
2054
2215
  if (command && handlers[command]) {
2216
+ var _hookStart = Date.now();
2055
2217
  try {
2056
2218
  await Promise.resolve(handlers[command]());
2057
2219
  } catch (e) {
2058
2220
  console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message);
2221
+ } finally {
2222
+ try { _recordHookLatency(command, Date.now() - _hookStart); } catch (_) {}
2059
2223
  }
2060
2224
  } else if (command) {
2061
2225
  console.log('[OK] Hook: ' + command);
2062
2226
  } else {
2063
- console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
2227
+ console.log('Usage: hook-handler.cjs <route|pre-bash|pre-search|post-edit|post-read|post-graph-tool|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
2064
2228
  }
2065
2229
  }
2066
2230
 
@@ -696,6 +696,30 @@ function getGraphifyStats() {
696
696
  return { nodes: 0, edges: 0, exists: false };
697
697
  }
698
698
 
699
+ // Hook latency — sum of mean times for hooks that fire on every prompt.
700
+ function getHookLatency() {
701
+ const p = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
702
+ try {
703
+ if (!fs.existsSync(p)) return null;
704
+ const d = JSON.parse(fs.readFileSync(p, 'utf-8'));
705
+ // Hooks that run per-prompt: route, pre-search, post-read, post-edit
706
+ const perPrompt = ['route'];
707
+ let totalMs = 0;
708
+ let count = 0;
709
+ for (const h of perPrompt) {
710
+ if (d[h] && d[h].mean) { totalMs += d[h].mean; count++; }
711
+ }
712
+ if (count === 0) return null;
713
+ // Find slowest hook
714
+ let slowest = null;
715
+ for (const k of Object.keys(d)) {
716
+ if (k === 'lastUpdated' || !d[k] || typeof d[k] !== 'object') continue;
717
+ if (!slowest || d[k].mean > slowest.mean) slowest = { name: k, mean: d[k].mean };
718
+ }
719
+ return { perPromptMs: totalMs, slowest };
720
+ } catch { return null; }
721
+ }
722
+
699
723
  // Graph usage telemetry — ratio of monograph_* vs Grep/Glob, plus $ saved
700
724
  function getGraphUsage() {
701
725
  const usagePath = path.join(CWD, '.monomind', 'metrics', 'graph-usage.json');
@@ -1270,7 +1294,16 @@ function generateDashboard() {
1270
1294
  usageStr = ` ${DIV} ${col}📊 graph ${usage.pct}%${x.reset}${x.slate} · grep ${100 - usage.pct}%${x.reset}${saved}`;
1271
1295
  }
1272
1296
 
1273
- lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}${usageStr}`);
1297
+ // Hook latency — surface when slow (>500ms per prompt)
1298
+ const lat = getHookLatency();
1299
+ let latStr = '';
1300
+ if (lat && lat.perPromptMs > 500) {
1301
+ latStr = ` ${DIV} ${x.coral}⚡ hooks ${lat.perPromptMs}ms${x.reset}`;
1302
+ } else if (lat && lat.perPromptMs > 0) {
1303
+ latStr = ` ${DIV} ${x.dim}⚡ ${lat.perPromptMs}ms${x.reset}`;
1304
+ }
1305
+
1306
+ lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}${usageStr}${latStr}`);
1274
1307
  lines.push(SEP);
1275
1308
 
1276
1309
  // ── Row 2: Graph freshness + Pending HIL ─────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"statusline-generator.d.ts","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8oCrE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8BnE"}
1
+ {"version":3,"file":"statusline-generator.d.ts","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA4qCrE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8BnE"}
@@ -828,6 +828,27 @@ function getTriggerStats() {
828
828
  } catch { return { triggers: 0, agents: 0 }; }
829
829
  }
830
830
 
831
+ // Hook latency — surface slow per-prompt hooks in the statusline.
832
+ function getHookLatency() {
833
+ const p = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
834
+ try {
835
+ if (!fs.existsSync(p)) return null;
836
+ const d = JSON.parse(fs.readFileSync(p, 'utf-8'));
837
+ const perPrompt = ['route'];
838
+ let totalMs = 0; let count = 0;
839
+ for (const h of perPrompt) {
840
+ if (d[h] && d[h].mean) { totalMs += d[h].mean; count++; }
841
+ }
842
+ if (count === 0) return null;
843
+ let slowest = null;
844
+ for (const k of Object.keys(d)) {
845
+ if (k === 'lastUpdated' || !d[k] || typeof d[k] !== 'object') continue;
846
+ if (!slowest || d[k].mean > slowest.mean) slowest = { name: k, mean: d[k].mean };
847
+ }
848
+ return { perPromptMs: totalMs, slowest: slowest };
849
+ } catch { return null; }
850
+ }
851
+
831
852
  // Graph usage telemetry — ratio of monograph_* vs Grep/Glob/Bash-grep, + $ saved
832
853
  function getGraphUsage() {
833
854
  const usagePath = path.join(CWD, '.monomind', 'metrics', 'graph-usage.json');
@@ -1096,7 +1117,16 @@ function generateDashboard() {
1096
1117
  usageStr = \` \${DIV} \${col}📊 graph \${usage.pct}%\${x.reset}\${x.slate} · grep \${100 - usage.pct}%\${x.reset}\${saved}\`;
1097
1118
  }
1098
1119
 
1099
- lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\${usageStr}\`);
1120
+ // Hook latency — surface when slow (>500ms per prompt)
1121
+ const lat = getHookLatency();
1122
+ let latStr = '';
1123
+ if (lat && lat.perPromptMs > 500) {
1124
+ latStr = \` \${DIV} \${x.coral}⚡ hooks \${lat.perPromptMs}ms\${x.reset}\`;
1125
+ } else if (lat && lat.perPromptMs > 0) {
1126
+ latStr = \` \${DIV} \${x.dim}⚡ \${lat.perPromptMs}ms\${x.reset}\`;
1127
+ }
1128
+
1129
+ lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\${usageStr}\${latStr}\`);
1100
1130
  lines.push(SEP);
1101
1131
 
1102
1132
  // ── Row 2: Graph freshness + Pending HIL ─────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"statusline-generator.js","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAoB;IAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;IAC5C,OAAO;;;;;;;;;;;;;;;;;;;;;;;eAuBM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAonCvB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAoB;IACzD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QAChC,OAAO,sCAAsC,CAAC;IAChD,CAAC;IAED,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAwBR,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"statusline-generator.js","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAoB;IAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;IAC5C,OAAO;;;;;;;;;;;;;;;;;;;;;;;eAuBM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkpCvB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAoB;IACzD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QAChC,OAAO,sCAAsC,CAAC;IAChD,CAAC;IAED,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAwBR,CAAC;AACF,CAAC"}