129 lines
4.1 KiB
JavaScript
129 lines
4.1 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* _observer-summary.js (v6.6-rc2 附件, 下划线前缀避开 baseline 保护)
|
||
|
|
* P0 观察期数据摘要 · 读 debug/agent-returns.jsonl 出统计
|
||
|
|
*
|
||
|
|
* 用法:
|
||
|
|
* node scripts/patches/_observer-summary.js # 人类可读
|
||
|
|
* node scripts/patches/_observer-summary.js --json # 机器可读
|
||
|
|
*
|
||
|
|
* 阈值: P1 开工门槛 = 业务样本 >= 100 条 (原规划)
|
||
|
|
* 业务样本 = 排除 session_id 以 'smoke-' / 'test-' 开头的冒烟记录
|
||
|
|
*/
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
const CLAUDE_ROOT = path.resolve(__dirname, '..', '..');
|
||
|
|
const LOG_FILE = path.join(CLAUDE_ROOT, 'debug', 'agent-returns.jsonl');
|
||
|
|
const THRESHOLD = 100;
|
||
|
|
const AS_JSON = process.argv.includes('--json');
|
||
|
|
|
||
|
|
function pct(part, whole) {
|
||
|
|
return whole === 0 ? '0.0%' : (part / whole * 100).toFixed(1) + '%';
|
||
|
|
}
|
||
|
|
|
||
|
|
function percentile(sorted, p) {
|
||
|
|
if (sorted.length === 0) return 0;
|
||
|
|
const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor(sorted.length * p)));
|
||
|
|
return sorted[idx];
|
||
|
|
}
|
||
|
|
|
||
|
|
function readRecords() {
|
||
|
|
if (!fs.existsSync(LOG_FILE)) return [];
|
||
|
|
const raw = fs.readFileSync(LOG_FILE, 'utf8');
|
||
|
|
const records = [];
|
||
|
|
for (const line of raw.split(/\r?\n/)) {
|
||
|
|
if (!line.trim()) continue;
|
||
|
|
try { records.push(JSON.parse(line)); } catch {}
|
||
|
|
}
|
||
|
|
return records;
|
||
|
|
}
|
||
|
|
|
||
|
|
function isSmoke(r) {
|
||
|
|
const sid = (r.session_id || '').toLowerCase();
|
||
|
|
return sid.startsWith('smoke-') || sid.startsWith('test-') || sid.includes('smoke');
|
||
|
|
}
|
||
|
|
|
||
|
|
function summarize(records) {
|
||
|
|
const total = records.length;
|
||
|
|
const smokes = records.filter(isSmoke);
|
||
|
|
const business = records.filter(r => !isSmoke(r));
|
||
|
|
|
||
|
|
const agentTypes = {};
|
||
|
|
const traceIdSet = new Set();
|
||
|
|
const lengths = [];
|
||
|
|
let tsMin = null, tsMax = null;
|
||
|
|
|
||
|
|
for (const r of business) {
|
||
|
|
const t = r.agent_type || '(empty)';
|
||
|
|
agentTypes[t] = (agentTypes[t] || 0) + 1;
|
||
|
|
if (r.traceId) traceIdSet.add(r.traceId);
|
||
|
|
if (typeof r.text_length === 'number') lengths.push(r.text_length);
|
||
|
|
const ts = r.ts;
|
||
|
|
if (ts) {
|
||
|
|
if (!tsMin || ts < tsMin) tsMin = ts;
|
||
|
|
if (!tsMax || ts > tsMax) tsMax = ts;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
lengths.sort((a, b) => a - b);
|
||
|
|
|
||
|
|
return {
|
||
|
|
total_records: total,
|
||
|
|
smoke_records: smokes.length,
|
||
|
|
business_records: business.length,
|
||
|
|
unique_traceIds: traceIdSet.size,
|
||
|
|
agent_type_distribution: agentTypes,
|
||
|
|
text_length_stats: lengths.length === 0 ? null : {
|
||
|
|
min: lengths[0],
|
||
|
|
max: lengths[lengths.length - 1],
|
||
|
|
avg: +(lengths.reduce((a, b) => a + b, 0) / lengths.length).toFixed(1),
|
||
|
|
p50: percentile(lengths, 0.50),
|
||
|
|
p90: percentile(lengths, 0.90),
|
||
|
|
},
|
||
|
|
time_span: { from: tsMin, to: tsMax },
|
||
|
|
threshold: THRESHOLD,
|
||
|
|
progress: pct(business.length, THRESHOLD),
|
||
|
|
p1_eligible: business.length >= THRESHOLD,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderHuman(s) {
|
||
|
|
const lines = [];
|
||
|
|
lines.push('=== v6.6-rc2 P0 观察期摘要 ===');
|
||
|
|
lines.push('');
|
||
|
|
lines.push('总条数 : ' + s.total_records + ' (冒烟 ' + s.smoke_records + ' + 业务 ' + s.business_records + ')');
|
||
|
|
lines.push('唯一 trace : ' + s.unique_traceIds);
|
||
|
|
lines.push('时间跨度 : ' + (s.time_span.from || '-') + ' ~ ' + (s.time_span.to || '-'));
|
||
|
|
lines.push('');
|
||
|
|
lines.push('agent_type 分布:');
|
||
|
|
const dist = s.agent_type_distribution;
|
||
|
|
const keys = Object.keys(dist).sort((a, b) => dist[b] - dist[a]);
|
||
|
|
if (keys.length === 0) lines.push(' (空)');
|
||
|
|
for (const k of keys) lines.push(' ' + k.padEnd(24) + dist[k]);
|
||
|
|
lines.push('');
|
||
|
|
if (s.text_length_stats) {
|
||
|
|
const t = s.text_length_stats;
|
||
|
|
lines.push('text_length: min=' + t.min + ' max=' + t.max + ' avg=' + t.avg + ' p50=' + t.p50 + ' p90=' + t.p90);
|
||
|
|
} else {
|
||
|
|
lines.push('text_length: (无业务样本)');
|
||
|
|
}
|
||
|
|
lines.push('');
|
||
|
|
lines.push('P1 开工门槛: ' + s.business_records + ' / ' + s.threshold + ' (' + s.progress + ')');
|
||
|
|
lines.push('P1 可开工 : ' + (s.p1_eligible ? 'YES' : 'NO · 继续观察'));
|
||
|
|
return lines.join('\n');
|
||
|
|
}
|
||
|
|
|
||
|
|
function main() {
|
||
|
|
const records = readRecords();
|
||
|
|
const s = summarize(records);
|
||
|
|
if (AS_JSON) {
|
||
|
|
process.stdout.write(JSON.stringify(s, null, 2));
|
||
|
|
} else {
|
||
|
|
process.stdout.write(renderHuman(s) + '\n');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
main();
|