@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.19

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.
Files changed (137) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +236 -3
  4. package/bin/gotry-inner.js +66 -20
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +15 -5
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/channel-health.js +103 -0
  11. package/dist/capabilities/channel-registry.js +213 -0
  12. package/dist/capabilities/doctor.js +285 -0
  13. package/dist/capabilities/effect.js +178 -5
  14. package/dist/capabilities/flyai.js +13 -0
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  16. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  17. package/dist/capabilities/session/extension-bridge.js +14 -1
  18. package/dist/capabilities/session/extension-channel.js +4 -2
  19. package/dist/capabilities/session/extension-distribution.js +1 -1
  20. package/dist/capabilities/session-search.js +311 -0
  21. package/dist/capabilities/visa-policy.js +126 -0
  22. package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
  23. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  24. package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
  25. package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
  26. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  27. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  28. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  29. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  30. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  31. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  32. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  33. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  34. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  35. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  36. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  37. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  38. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  39. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  40. package/dist/scripts/bootstrap-tests.js +87 -2
  41. package/dist/scripts/build-metrics-report.js +339 -0
  42. package/dist/scripts/channel-probe-tests.js +148 -0
  43. package/dist/scripts/channel-probe.js +252 -0
  44. package/dist/scripts/channel-registry-tests.js +262 -0
  45. package/dist/scripts/doctor-tests.js +121 -0
  46. package/dist/scripts/effect-tests.js +274 -1
  47. package/dist/scripts/extension-tests.js +129 -11
  48. package/dist/scripts/fact-gate-tests.js +91 -2
  49. package/dist/scripts/flyai-tests.js +17 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  51. package/dist/scripts/metrics-report-tests.js +288 -0
  52. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  53. package/dist/scripts/session-tests.js +401 -1
  54. package/dist/scripts/smoke.js +100 -83
  55. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  56. package/dist/scripts/turn-handoff-collect.js +189 -0
  57. package/dist/scripts/turn-policy-tests.js +56 -0
  58. package/dist/scripts/typed-contract-canary.js +261 -0
  59. package/dist/scripts/visa-policy-tests.js +75 -0
  60. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  61. package/dist/src/artifact-gate.js +91 -3
  62. package/dist/src/benchmark-agent-conformance.js +7 -4
  63. package/dist/src/benchmark-environment-bridge.js +68 -7
  64. package/dist/src/bookable-facts.js +62 -3
  65. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  66. package/dist/src/booking-surface/canonical-schema.js +8 -22
  67. package/dist/src/booking-surface/contracts.js +70 -19
  68. package/dist/src/booking-surface/dsh-planner.js +310 -177
  69. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  70. package/dist/src/booking-surface/index.js +1 -5
  71. package/dist/src/booking-surface/profile.js +1 -1
  72. package/dist/src/booking-surface/runtime.js +1682 -247
  73. package/dist/src/booking-surface/server.js +371 -185
  74. package/dist/src/booking-surface/startup.js +17 -27
  75. package/dist/src/booking-surface/validation.js +270 -760
  76. package/dist/src/index.js +735 -149
  77. package/dist/src/turn-deadline.js +292 -0
  78. package/dist/src/turn-policy.js +146 -0
  79. package/dist/src/wish-pool.js +5 -0
  80. package/extension/background.js +23 -4
  81. package/extension/content-main.js +47 -9
  82. package/extension/manifest.json +27 -9
  83. package/package.json +6 -26
  84. package/schemas/booking.surface.schema.json +2593 -0
  85. package/ts/capabilities/agent-reach.ts +4 -4
  86. package/ts/capabilities/flyai.ts +21 -3
  87. package/ts/capabilities/session/action-cache.ts +1 -1
  88. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  89. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  90. package/ts/capabilities/session/extension-bridge.ts +32 -2
  91. package/ts/capabilities/session/extension-channel.ts +9 -8
  92. package/ts/capabilities/session/extension-distribution.ts +3 -1
  93. package/ts/capabilities/session/health-watch.ts +1 -1
  94. package/ts/capabilities/session/read-guard.ts +1 -1
  95. package/ts/capabilities/session/wizard.ts +1 -1
  96. package/ts/capabilities/session-search.ts +323 -1
  97. package/ts/package.json +1 -0
  98. package/ts/scripts/turn-handoff-collect.ts +178 -0
  99. package/ts/src/artifact-gate.ts +82 -2
  100. package/ts/src/benchmark-agent-conformance.ts +14 -6
  101. package/ts/src/benchmark-environment-bridge.ts +29 -10
  102. package/ts/src/bookable-facts.ts +109 -5
  103. package/ts/src/booking-saga.ts +1 -1
  104. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  105. package/ts/src/booking-surface/canonical-schema.js +8 -22
  106. package/ts/src/booking-surface/contracts.ts +278 -239
  107. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  108. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  109. package/ts/src/booking-surface/index.ts +1 -5
  110. package/ts/src/booking-surface/profile.ts +11 -11
  111. package/ts/src/booking-surface/runtime.ts +1351 -351
  112. package/ts/src/booking-surface/server.ts +282 -175
  113. package/ts/src/booking-surface/startup.ts +36 -48
  114. package/ts/src/booking-surface/validation.ts +194 -442
  115. package/ts/src/contracts.ts +1 -1
  116. package/ts/src/index.ts +489 -171
  117. package/ts/src/loop.ts +1 -1
  118. package/ts/src/state-ledger.ts +1 -1
  119. package/ts/src/turn-deadline.ts +361 -0
  120. package/ts/src/turn-policy.ts +154 -0
  121. package/ts/src/wish-pool.ts +17 -4
  122. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  123. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  124. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  125. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  126. package/dist/src/booking-surface/contracts-v2.js +0 -89
  127. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  128. package/dist/src/booking-surface/server-v2.js +0 -334
  129. package/dist/src/booking-surface/validation-v2.js +0 -319
  130. package/dist/src/tool-budget.js +0 -136
  131. package/schemas/booking.surface.v1.schema.json +0 -927
  132. package/schemas/booking.surface.v2.schema.json +0 -61
  133. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  134. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  135. package/ts/src/booking-surface/server-v2.ts +0 -247
  136. package/ts/src/booking-surface/validation-v2.ts +0 -205
  137. package/ts/src/tool-budget.ts +0 -165
@@ -1,5 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
- import { execFileSync } from 'node:child_process';
2
+ import { execFileSync, spawnSync } from 'node:child_process';
3
+ import { mkdtempSync, readFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
3
5
  import { join } from 'node:path';
4
6
  const repoRoot = join(import.meta.dirname, '..', '..');
5
7
  const bootstrap = join(repoRoot, 'bin', 'gotry-bootstrap.js');
@@ -102,7 +104,90 @@ const c7b = runBootstrap([
102
104
  assert.equal(c7b.code, 0, `非法 --extension-from 值应回落 bundled 且 exit 0,实际 ${c7b.code}\n${c7b.out}`);
103
105
  assert.ok(!c7b.out.includes('下载通道'), '非法值不应进入 github 通道');
104
106
  console.log('7. 扩展分发 github 通道(不可达基址即时降级 + 非法值回落 bundled)OK');
105
- console.log('BOOTSTRAP TESTS: 7/7 OK(扩展就位 + 跳过开关 / wizard --dry-run / wizard 真实 / 扩展分发通道 / 显式跳过 + auto 跳过 + 单项跳过)');
107
+ const c8 = runBootstrap([
108
+ 'doctor'
109
+ ], {});
110
+ assert.ok(c8.out.includes('[gotry-doctor]'), '应输出 [gotry-doctor] 标签');
111
+ assert.ok(c8.out.includes('Agent Reach'), '体检应含 agent-reach 项');
112
+ assert.ok(c8.out.includes('hbcli'), '体检应含 hbcli 项');
113
+ assert.ok(c8.out.includes('FLYAI_API_KEY'), '体检应含 flyai key 项(试用额度降级面)');
114
+ assert.ok(c8.out.includes('LLM key'), '体检应含 LLM key 让渡说明(doctor 不管 key)');
115
+ assert.ok(c8.out.includes('dsh-map-tools'), '体检应含 dsh-map-tools 项(#139;与 ts/capabilities/doctor.ts 两面成对)');
116
+ assert.ok(c8.out.includes('dsh-tool-ask-user'), '体检应含 dsh-tool-ask-user 项(与 ts/capabilities/doctor.ts 两面成对)');
117
+ assert.ok(c8.out.includes('doctor-report.md'), '应提示报告落盘路径');
118
+ assert.ok([
119
+ 0,
120
+ 1
121
+ ].includes(c8.code), `doctor exit 应为 0(就绪)或 1(有缺失),实际 ${c8.code}`);
122
+ const reportPath = join(repoRoot, 'gotry-state', 'doctor-report.md');
123
+ const report = readFileSync(reportPath, 'utf-8');
124
+ assert.ok(report.includes('# GoTry 依赖体检报告'), '报告 markdown 应落盘可预览');
125
+ assert.ok(report.includes('npx gotry doctor --fix'), '报告应带补装指引');
126
+ console.log('8. doctor 子命令(体检清单 + LLM key 让渡 + 报告落盘)OK');
127
+ const calHome = mkdtempSync(join(tmpdir(), 'gotry-cal-test-'));
128
+ const calEnv = {
129
+ HOME: calHome
130
+ };
131
+ const c9a = runBootstrap([
132
+ 'calendar',
133
+ '--status'
134
+ ], calEnv);
135
+ assert.equal(c9a.code, 0, `calendar --status(默认态)应 exit 0\n${c9a.out}`);
136
+ assert.ok(c9a.out.includes('默认未挂载'), '默认态=未挂载');
137
+ assert.ok(c9a.out.includes(join(calHome, '.gotry', 'calendar.json')), '状态文件路径可见');
138
+ const c9b = runBootstrap([
139
+ 'calendar'
140
+ ], calEnv);
141
+ assert.equal(c9b.code, 0, `calendar(开启)应 exit 0\n${c9b.out}`);
142
+ assert.ok(c9b.out.includes('已开启挂载'), '开启态输出');
143
+ assert.ok(c9b.out.includes('cordis.patch.yml'), '未配置时给 profile 配置指引');
144
+ const c9c = runBootstrap([
145
+ 'calendar',
146
+ '--status'
147
+ ], calEnv);
148
+ assert.ok(c9c.out.includes('已挂载'), '开启后 --status 显示已挂载');
149
+ const c9d = runBootstrap([
150
+ 'doctor'
151
+ ], {
152
+ ...calEnv,
153
+ GOTRY_SETUP_SKIP: '1'
154
+ });
155
+ assert.ok(c9d.out.includes('dsh-calendar'), 'doctor 清单含 calendar 项(setup 状态面)');
156
+ assert.ok(c9d.out.includes('已挂载但 calendar 未配置'), '开启未配置=doctor 可见');
157
+ const c9e = runBootstrap([
158
+ 'calendar',
159
+ '--off'
160
+ ], calEnv);
161
+ assert.equal(c9e.code, 0, `calendar --off 应 exit 0\n${c9e.out}`);
162
+ assert.ok(c9e.out.includes('恢复默认不挂载'), '关闭态输出');
163
+ const c9f = runBootstrap([
164
+ 'calendar',
165
+ '--status'
166
+ ], calEnv);
167
+ assert.ok(c9f.out.includes('默认未挂载'), '关闭后回到默认态');
168
+ console.log('9. calendar 子命令(setup 状态面 on/off/status + doctor 三态,HOME 隔离)OK');
169
+ {
170
+ const sumHome = mkdtempSync(join(tmpdir(), 'gotry-sum-test-'));
171
+ const r = spawnSync('node', [
172
+ bootstrap,
173
+ 'doctor',
174
+ '--summary'
175
+ ], {
176
+ encoding: 'utf-8',
177
+ timeout: 60_000,
178
+ env: {
179
+ ...process.env,
180
+ HOME: sumHome
181
+ }
182
+ });
183
+ assert.equal(r.status, 0, `doctor --summary 应恒 exit 0(不挡启动),实际 ${r.status}\n${r.stderr}`);
184
+ assert.ok(!r.stdout.includes('报告已写'), 'summary 模式零写盘(不落 doctor-report.md)');
185
+ assert.ok(!r.stdout.includes('[gotry-doctor]'), 'summary 模式零 header(stdout 静默)');
186
+ assert.match(r.stderr, /\[gotry\] doctor: \d+ 项待处理/, '隔离 HOME 有缺失项 → 一行摘要进 stderr');
187
+ assert.match(r.stderr, /gotry_doctor/, '摘要带对话内指路');
188
+ assert.match(r.stderr, /扩展=缺/, '缺失项人话=缺(降级类=半可用)');
189
+ }console.log('10. 启动一次性 doctor 摘要(--summary:stderr 一行/零写盘/恒 exit 0)OK');
190
+ console.log('BOOTSTRAP TESTS: 10/10 OK(扩展就位 + 跳过开关 / wizard --dry-run / wizard 真实 / 扩展分发通道 / doctor 体检面 / calendar setup 状态面 / 显式跳过 + auto 跳过 + 单项跳过 / 启动摘要)');
106
191
 
107
192
 
108
193
  //# sourceURL=ts/scripts/bootstrap-tests.ts
@@ -0,0 +1,339 @@
1
+ import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { isAbsolute, join, resolve } from 'node:path';
3
+ const BRIDGE_LATENCY_BUDGET_MS = 500;
4
+ const CHANNEL_WINDOW_DAYS = 30;
5
+ function stateFile(stateRoot, name) {
6
+ const root = stateRoot === '.' ? process.cwd() : stateRoot;
7
+ return join(root, 'gotry-state', name);
8
+ }
9
+ function readJsonl(path) {
10
+ if (!existsSync(path)) return [];
11
+ try {
12
+ const rows = [];
13
+ for (const line of readFileSync(path, 'utf-8').split('\n')){
14
+ const t = line.trim();
15
+ if (!t) continue;
16
+ try {
17
+ rows.push(JSON.parse(t));
18
+ } catch {}
19
+ }
20
+ return rows;
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+ function bounded(value, max = 120) {
26
+ if (!value) return undefined;
27
+ return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
28
+ }
29
+ const BLOCKED_VERDICTS = new Set([
30
+ 'error',
31
+ 'needs-setup'
32
+ ]);
33
+ export function aggregateFacts(rows) {
34
+ const latest = new Map();
35
+ let lines = 0;
36
+ for (const raw of rows){
37
+ const row = typeof raw === 'object' && raw !== null ? raw : {};
38
+ lines += 1;
39
+ const id = typeof row.fact_id === 'string' ? row.fact_id : '';
40
+ if (!id) continue;
41
+ latest.set(id, row);
42
+ }
43
+ const byKind = {};
44
+ for (const row of latest.values()){
45
+ const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
46
+ const verdict = typeof row.verdict === 'string' && row.verdict ? row.verdict : 'n/a';
47
+ const slot = byKind[kind] ??= {
48
+ total: 0,
49
+ verdicts: {},
50
+ blocked: 0
51
+ };
52
+ slot.total += 1;
53
+ slot.verdicts[verdict] = (slot.verdicts[verdict] ?? 0) + 1;
54
+ if (BLOCKED_VERDICTS.has(verdict)) slot.blocked += 1;
55
+ }
56
+ return {
57
+ lines,
58
+ deduped: latest.size,
59
+ byKind
60
+ };
61
+ }
62
+ export function aggregateChannels(rows, opts = {}) {
63
+ const nowMs = (opts.now ?? new Date()).getTime();
64
+ const cutoff = nowMs - (opts.windowDays ?? CHANNEL_WINDOW_DAYS) * 86_400_000;
65
+ const counts = new Map();
66
+ const newest = new Map();
67
+ const lastState = new Map();
68
+ for (const raw of rows){
69
+ const row = typeof raw === 'object' && raw !== null ? raw : {};
70
+ if (typeof row.channel !== 'string' || !row.channel) continue;
71
+ if (typeof row.state === 'string' && [
72
+ 'down',
73
+ 'cooldown',
74
+ 'ok'
75
+ ].includes(row.state)) {
76
+ lastState.set(row.channel, row.state);
77
+ }
78
+ }
79
+ for (const raw of rows){
80
+ const row = typeof raw === 'object' && raw !== null ? raw : {};
81
+ if (typeof row.channel !== 'string' || !row.channel) continue;
82
+ if (row.state !== 'down' && row.state !== 'cooldown') continue;
83
+ if (lastState.get(row.channel) === 'ok') continue;
84
+ const at = typeof row.at === 'string' ? row.at : '';
85
+ const atMs = Date.parse(at);
86
+ if (Number.isFinite(atMs) && atMs < cutoff) continue;
87
+ counts.set(row.channel, (counts.get(row.channel) ?? 0) + 1);
88
+ const atMsSafe = Number.isFinite(atMs) ? atMs : 0;
89
+ const prev = newest.get(row.channel);
90
+ if (prev && prev.atMs >= atMsSafe) continue;
91
+ newest.set(row.channel, {
92
+ channel: row.channel,
93
+ state: row.state,
94
+ reason: typeof row.reason === 'string' ? row.reason : undefined,
95
+ at: at || undefined,
96
+ events: 0,
97
+ atMs: atMsSafe
98
+ });
99
+ }
100
+ const latest = [
101
+ ...newest.values()
102
+ ].map(({ atMs: _atMs, ...row })=>({
103
+ ...row,
104
+ events: counts.get(row.channel) ?? 0
105
+ }));
106
+ return {
107
+ windowDays: opts.windowDays ?? CHANNEL_WINDOW_DAYS,
108
+ latest
109
+ };
110
+ }
111
+ export function aggregateIncidents(rows, opts = {}) {
112
+ const nowMs = (opts.now ?? new Date()).getTime();
113
+ const windowDays = opts.windowDays ?? 7;
114
+ const cutoff = nowMs - windowDays * 86_400_000;
115
+ const byKind = {};
116
+ const sources = new Map();
117
+ let total = 0;
118
+ for (const raw of rows){
119
+ const row = typeof raw === 'object' && raw !== null ? raw : {};
120
+ const ts = typeof row.ts === 'string' ? row.ts : '';
121
+ const atMs = Date.parse(ts);
122
+ if (Number.isFinite(atMs) && atMs < cutoff) continue;
123
+ const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
124
+ const slot = byKind[kind] ??= {
125
+ count: 0
126
+ };
127
+ slot.count += 1;
128
+ if (!slot.lastTs || Number.isFinite(atMs) && atMs > Date.parse(slot.lastTs)) {
129
+ slot.lastTs = ts || slot.lastTs;
130
+ slot.lastMessage = bounded(typeof row.message === 'string' ? row.message : undefined);
131
+ }
132
+ const source = typeof row.source === 'string' && row.source ? row.source : '(unknown)';
133
+ sources.set(source, (sources.get(source) ?? 0) + 1);
134
+ total += 1;
135
+ }
136
+ const topSources = [
137
+ ...sources.entries()
138
+ ].sort((a, b)=>b[1] - a[1]).slice(0, 5).map(([source, count])=>({
139
+ source,
140
+ count
141
+ }));
142
+ return {
143
+ windowDays,
144
+ totalInWindow: total,
145
+ byKind,
146
+ topSources
147
+ };
148
+ }
149
+ export function aggregateLatency(rows, opts = {}) {
150
+ const nowMs = (opts.now ?? new Date()).getTime();
151
+ const cutoff = nowMs - (opts.windowDays ?? 7) * 86_400_000;
152
+ const values = [];
153
+ const kinds = new Map();
154
+ for (const raw of rows){
155
+ const row = typeof raw === 'object' && raw !== null ? raw : {};
156
+ const ts = typeof row.ts === 'string' ? row.ts : '';
157
+ const atMs = Date.parse(ts);
158
+ if (Number.isFinite(atMs) && atMs < cutoff) continue;
159
+ const ms = typeof row.latencyMs === 'number' ? row.latencyMs : Number.NaN;
160
+ if (!Number.isFinite(ms)) continue;
161
+ values.push(ms);
162
+ const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
163
+ kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
164
+ }
165
+ const sorted = [
166
+ ...values
167
+ ].sort((a, b)=>a - b);
168
+ const pick = (q)=>sorted.length === 0 ? 0 : sorted[Math.min(sorted.length - 1, Math.ceil(q * sorted.length) - 1)];
169
+ return {
170
+ count: sorted.length,
171
+ p50: pick(0.5),
172
+ p95: pick(0.95),
173
+ max: sorted.length === 0 ? 0 : sorted[sorted.length - 1],
174
+ overBudget: sorted.filter((ms)=>ms > BRIDGE_LATENCY_BUDGET_MS).length,
175
+ budgetMs: BRIDGE_LATENCY_BUDGET_MS,
176
+ topKinds: [
177
+ ...kinds.entries()
178
+ ].sort((a, b)=>b[1] - a[1]).slice(0, 5).map(([kind, count])=>({
179
+ kind,
180
+ count
181
+ }))
182
+ };
183
+ }
184
+ function presenceOf(path) {
185
+ try {
186
+ if (!existsSync(path)) return {
187
+ exists: false
188
+ };
189
+ const st = statSync(path);
190
+ return {
191
+ exists: true,
192
+ bytes: st.size,
193
+ mtime: st.mtime.toISOString()
194
+ };
195
+ } catch {
196
+ return {
197
+ exists: false
198
+ };
199
+ }
200
+ }
201
+ export async function collectMetrics(stateRoot, opts = {}) {
202
+ const root = resolve(stateRoot === '.' ? process.cwd() : stateRoot);
203
+ return {
204
+ generatedAt: (opts.now ?? new Date()).toISOString(),
205
+ stateRoot: root,
206
+ facts: aggregateFacts(readJsonl(stateFile(stateRoot, 'bookable-facts.jsonl'))),
207
+ channels: aggregateChannels(readJsonl(stateFile(stateRoot, 'channel-health.jsonl')), {
208
+ now: opts.now,
209
+ windowDays: CHANNEL_WINDOW_DAYS
210
+ }),
211
+ incidents: aggregateIncidents(readJsonl(stateFile(stateRoot, 'incidents.jsonl')), {
212
+ now: opts.now,
213
+ windowDays: opts.days ?? 7
214
+ }),
215
+ latency: aggregateLatency(readJsonl(stateFile(stateRoot, 'bridge-latency.jsonl')), {
216
+ now: opts.now,
217
+ windowDays: opts.days ?? 7
218
+ }),
219
+ ledger: presenceOf(stateFile(stateRoot, 'gotry-state.db')),
220
+ doctorReport: presenceOf(stateFile(stateRoot, 'doctor-report.md'))
221
+ };
222
+ }
223
+ function fmtRate(blocked, total) {
224
+ return total === 0 ? '—' : `${blocked}/${total}(${(blocked / total * 100).toFixed(0)}%)`;
225
+ }
226
+ function fmtBytes(bytes) {
227
+ if (bytes === undefined) return '—';
228
+ return bytes >= 1_048_576 ? `${(bytes / 1_048_576).toFixed(1)}MB` : `${bytes}B`;
229
+ }
230
+ export function renderMetricsReport(s) {
231
+ const L = [];
232
+ L.push(`# GoTry 质量指标报告(只读聚合,#138 第一切片)`);
233
+ L.push('');
234
+ L.push(`- 生成时间:${s.generatedAt}`);
235
+ L.push(`- stateRoot:\`${s.stateRoot}\`(全程只读,零 SQLite 打开)`);
236
+ L.push(`- 口径:工程面交付,不构成 M3 Exit 证据(Exit 证据归 issue #22)`);
237
+ L.push('');
238
+ L.push('## 事实闸 verdict 分布(bookable-facts.jsonl,同 fact_id 取最新)');
239
+ L.push('');
240
+ const kinds = Object.entries(s.facts.byKind);
241
+ if (kinds.length === 0) {
242
+ L.push('(空:无事实落盘)');
243
+ } else {
244
+ L.push('| kind | 去重后 | verdict 分布 | blocked(error+needs-setup) |');
245
+ L.push('|---|---|---|---|');
246
+ for (const [kind, st] of kinds){
247
+ const dist = Object.entries(st.verdicts).map(([v, n])=>`${v}:${n}`).join(' / ');
248
+ L.push(`| ${kind} | ${st.total} | ${dist} | ${fmtRate(st.blocked, st.total)} |`);
249
+ }
250
+ L.push('');
251
+ L.push(`原始行 ${s.facts.lines},去重后 ${s.facts.deduped}(重查重放幂等)。`);
252
+ }
253
+ L.push('');
254
+ L.push(`## 通道健康(channel-health.jsonl,${s.channels.windowDays} 天窗;hit 即清除,此处只见当前处于 down/cooldown 的通道;'ok' 恢复事件已超越)`);
255
+ L.push('');
256
+ if (s.channels.latest.length === 0) {
257
+ L.push('(空:窗口内无 down/cooldown 事件)');
258
+ } else {
259
+ L.push('| 通道 | 最新状态 | 原因 | 时间 | 窗口事件 |');
260
+ L.push('|---|---|---|---|---|');
261
+ for (const c of s.channels.latest){
262
+ L.push(`| ${c.channel} | ${c.state} | ${bounded(c.reason ?? '', 60) ?? '—'} | ${c.at ?? '—'} | ${c.events} |`);
263
+ }
264
+ }
265
+ L.push('');
266
+ L.push(`## 事故面(incidents.jsonl,${s.incidents.windowDays} 天窗)`);
267
+ L.push('');
268
+ const incidentKinds = Object.entries(s.incidents.byKind);
269
+ if (incidentKinds.length === 0) {
270
+ L.push('(空:窗口内无事故)');
271
+ } else {
272
+ L.push('| kind | 次数 | 最近一次 |');
273
+ L.push('|---|---|---|');
274
+ for (const [kind, st] of incidentKinds){
275
+ L.push(`| ${kind} | ${st.count} | ${st.lastTs ?? '—'}${st.lastMessage ? ` · ${st.lastMessage}` : ''} |`);
276
+ }
277
+ const src = s.incidents.topSources.map((t)=>`${t.source}×${t.count}`).join(', ');
278
+ if (src) {
279
+ L.push('');
280
+ L.push(`Top 来源:${src}`);
281
+ }
282
+ }
283
+ L.push('');
284
+ L.push(`## 桥延迟(bridge-latency.jsonl,${s.incidents.windowDays} 天窗;预算 ${s.latency.budgetMs}ms,超限即复审锚点)`);
285
+ L.push('');
286
+ if (s.latency.count === 0) {
287
+ L.push('(空:窗口内无延迟记录)');
288
+ } else {
289
+ const kinds = s.latency.topKinds.map((t)=>`${t.kind}×${t.count}`).join(', ');
290
+ L.push(`样本 ${s.latency.count}:p50=${s.latency.p50}ms · p95=${s.latency.p95}ms · max=${s.latency.max}ms · 超预算(>${s.latency.budgetMs}ms)= **${s.latency.overBudget}**`);
291
+ if (kinds) L.push(`按 kind:${kinds}`);
292
+ }
293
+ L.push('');
294
+ L.push('## 账本与体检报告');
295
+ L.push('');
296
+ L.push(`- 账本 gotry-state.db:${s.ledger.exists ? `存在(${fmtBytes(s.ledger.bytes)},更新 ${s.ledger.mtime};明细经 \`npx gotry state\` 查看,指标面不打开 SQLite)` : '不存在(该 stateRoot 无账本)'}`);
297
+ L.push(`- doctor 报告 doctor-report.md:${s.doctorReport.exists ? `存在(更新 ${s.doctorReport.mtime})` : '不存在(跑 `npx gotry doctor` 生成)'}`);
298
+ L.push('');
299
+ L.push('## 评测三层入口(ADR-11;运行结果由 run-all-tests.sh / CI 承载,v1 不在此重跑)');
300
+ L.push('');
301
+ L.push('- 单元/合同/golden E2E:`./scripts/run-all-tests.sh`(全绿是任何提交前闸)');
302
+ L.push('- 合同层样例:`evaluation-contract-tests.ts`;golden E2E:`benchmark-environment-bridge-e2e.ts`;typed 契约 canary:`typed-contract-canary.ts`');
303
+ L.push('');
304
+ return L.join('\n');
305
+ }
306
+ export function parseArgs(argv) {
307
+ let stateRoot = '.';
308
+ let out;
309
+ let days = 7;
310
+ for(let i = 0; i < argv.length; i += 1){
311
+ const a = argv[i];
312
+ if (a === '--state-root') stateRoot = argv[++i] ?? stateRoot;
313
+ else if (a === '--out') out = argv[++i];
314
+ else if (a === '--days') days = Math.max(1, Number.parseInt(argv[++i] ?? '7', 10) || 7);
315
+ }
316
+ return {
317
+ stateRoot: isAbsolute(stateRoot) ? stateRoot : resolve(stateRoot),
318
+ out,
319
+ days
320
+ };
321
+ }
322
+ export async function main(argv) {
323
+ const { stateRoot, out, days } = parseArgs(argv);
324
+ const snapshot = await collectMetrics(stateRoot, {
325
+ days
326
+ });
327
+ const markdown = renderMetricsReport(snapshot);
328
+ if (out) {
329
+ writeFileSync(out, markdown + '\n', 'utf-8');
330
+ process.stdout.write(`指标报告已写 ${out}(stateRoot 只读未动)\n`);
331
+ } else {
332
+ process.stdout.write(markdown + '\n');
333
+ }
334
+ }
335
+ const invokedDirectly = process.argv[1] && import.meta.url === new URL(`file://${resolve(process.argv[1])}`).href;
336
+ if (invokedDirectly) await main(process.argv.slice(2));
337
+
338
+
339
+ //# sourceURL=ts/scripts/build-metrics-report.ts
@@ -0,0 +1,148 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, writeFile, mkdir, readFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { evaluateProbeResults, defaultProbes, parseArgs, main } from './channel-probe.js';
6
+ import { readLatestChannelEvents, recordChannelEvent } from '../capabilities/channel-health.js';
7
+ import { aggregateChannels } from './build-metrics-report.js';
8
+ const tmp = await mkdtemp(join(tmpdir(), 'gotry-probe-test-'));
9
+ const probes = defaultProbes();
10
+ const hb = probes.find((p)=>p.channel === 'hbcli-hotel');
11
+ const om = probes.find((p)=>p.channel === 'open-meteo');
12
+ const fly = probes.find((p)=>p.channel === 'flyai');
13
+ const now = new Date('2026-09-07T12:00:00Z');
14
+ const evs = evaluateProbeResults([
15
+ {
16
+ probe: hb,
17
+ result: {
18
+ ok: false,
19
+ reason: 'auth-denied',
20
+ ms: 5
21
+ }
22
+ },
23
+ {
24
+ probe: om,
25
+ result: {
26
+ ok: true,
27
+ ms: 100
28
+ }
29
+ },
30
+ {
31
+ probe: fly,
32
+ result: {
33
+ ok: true,
34
+ ms: 0
35
+ }
36
+ }
37
+ ], new Set([
38
+ 'open-meteo'
39
+ ]), now);
40
+ assert.equal(evs.length, 2, `fail→down + ok恢复→'ok' 共 2 条,实际 ${evs.length}`);
41
+ assert.deepEqual({
42
+ channel: evs[0].channel,
43
+ state: evs[0].state,
44
+ reason: evs[0].reason
45
+ }, {
46
+ channel: 'hbcli-hotel',
47
+ state: 'down',
48
+ reason: 'auth-denied'
49
+ }, 'fail 应产 down 事件带 reason');
50
+ assert.deepEqual({
51
+ channel: evs[1].channel,
52
+ state: evs[1].state
53
+ }, {
54
+ channel: 'open-meteo',
55
+ state: 'ok'
56
+ }, 'ok 且此前 down 应产 ok 恢复事件');
57
+ const noWrites = evaluateProbeResults([
58
+ {
59
+ probe: om,
60
+ result: {
61
+ ok: true,
62
+ ms: 90
63
+ }
64
+ }
65
+ ], new Set(), now);
66
+ assert.equal(noWrites.length, 0, 'ok 且此前健康 → 零写放大');
67
+ const stateRoot = join(tmp, 'state');
68
+ await mkdir(join(stateRoot, 'gotry-state'), {
69
+ recursive: true
70
+ });
71
+ const jsonl = join(stateRoot, 'gotry-state', 'channel-health.jsonl');
72
+ await writeFile(jsonl, [
73
+ JSON.stringify({
74
+ channel: 'opensky',
75
+ state: 'down',
76
+ reason: 'http-503',
77
+ at: '2026-09-07T10:00:00.000Z'
78
+ }),
79
+ JSON.stringify({
80
+ channel: 'opensky',
81
+ state: 'ok',
82
+ at: '2026-09-07T11:00:00.000Z'
83
+ }),
84
+ JSON.stringify({
85
+ channel: 'hbcli-hotel',
86
+ state: 'down',
87
+ reason: 'auth-denied',
88
+ at: '2026-09-07T10:30:00.000Z'
89
+ }),
90
+ 'not-json-garbage',
91
+ ''
92
+ ].join('\n'), 'utf-8');
93
+ const latest = await readLatestChannelEvents(stateRoot, {
94
+ now: Date.parse('2026-09-07T12:00:00Z')
95
+ });
96
+ assert.equal(latest.get('opensky')?.state, 'ok', "'ok' 应超越同通道更早的 down");
97
+ assert.equal(latest.get('hbcli-hotel')?.state, 'down', '未恢复通道保持 down');
98
+ assert.equal(latest.size, 2, '坏行跳过,latest 只含两通道');
99
+ const openskyEv = latest.get('opensky');
100
+ assert.ok(openskyEv && openskyEv.state !== 'down', 'doctor 的 down 判定应不再命中已恢复通道');
101
+ const rows = (await readFile(jsonl, 'utf-8')).split('\n').filter((l)=>l.trim()).flatMap((l)=>{
102
+ try {
103
+ return [
104
+ JSON.parse(l)
105
+ ];
106
+ } catch {
107
+ return [];
108
+ }
109
+ });
110
+ const stats = aggregateChannels(rows, {
111
+ now: new Date('2026-09-07T12:00:00Z'),
112
+ windowDays: 30
113
+ });
114
+ const openskyStat = stats.latest.find((c)=>c.channel === 'opensky');
115
+ assert.equal(openskyStat, undefined, 'ok 超越的通道不应出现在当前 down/cooldown newest 面');
116
+ const hbStat = stats.latest.find((c)=>c.channel === 'hbcli-hotel');
117
+ assert.ok(hbStat && hbStat.state === 'down', '未恢复通道仍在 newest 面');
118
+ const listed = defaultProbes();
119
+ assert.ok(listed.find((p)=>p.channel === 'flyai')?.probeable === false, 'flyai 默认不探(共享额度)');
120
+ assert.ok(listed.filter((p)=>p.channel.startsWith('session:')).every((p)=>!p.probeable), 'session:* 全部 skip');
121
+ assert.deepEqual(parseArgs([
122
+ '--state-root',
123
+ '/tmp/x',
124
+ '--only',
125
+ 'a, b',
126
+ '--dry-run'
127
+ ]), {
128
+ stateRoot: '/tmp/x',
129
+ only: [
130
+ 'a',
131
+ 'b'
132
+ ],
133
+ dryRun: true,
134
+ list: false
135
+ }, 'parseArgs 解析');
136
+ const rcList = await main([
137
+ '--list'
138
+ ]);
139
+ assert.equal(rcList, 0, '--list exit 0');
140
+ const rcBad = await main([
141
+ '--only',
142
+ 'no-such-channel'
143
+ ]);
144
+ assert.equal(rcBad, 2, '--only 未匹配 exit 2');
145
+ console.log('CHANNEL PROBE TESTS: 5/5 OK(evaluate 纯函数 / latest-wins 恢复 / doctor 口径 / metrics 超越 / CLI 面)');
146
+
147
+
148
+ //# sourceURL=ts/scripts/channel-probe-tests.ts