@danceiny/gotry 0.0.1-rc.17 → 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 +185 -148
  2. package/README.zh-CN.md +195 -154
  3. package/bin/gotry-bootstrap.js +246 -26
  4. package/bin/gotry-inner.js +68 -38
  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 +115 -44
  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
@@ -15,6 +15,7 @@
15
15
 
16
16
  import {
17
17
  flightClaimVerdict,
18
+ hotelClaimVerdict,
18
19
  itineraryInvariants,
19
20
  latestFactsForRouteDate,
20
21
  type BookableFact,
@@ -62,12 +63,24 @@ export interface ExtractedAirportClaim {
62
63
  city: string
63
64
  }
64
65
 
66
+ /** 酒店 claim(D-26):断言「有房/可订/已核验」的行——目的地与档期缺失时 fail-closed */
67
+ export interface ExtractedHotelClaim {
68
+ line: number
69
+ text: string
70
+ destination?: string
71
+ check_in?: string
72
+ check_out?: string
73
+ }
74
+
65
75
  export interface ExtractedClaims {
66
76
  flights: ExtractedFlightClaim[]
67
77
  policies: ExtractedPolicyClaim[]
68
78
  airports: ExtractedAirportClaim[]
69
79
  /** 含「直飞」断言且可解析航线的行(通用直飞规则:该日无任何在架直飞即违例) */
70
80
  direct_lines: Array<{ line: number; text: string; origin?: string; destination?: string; date?: string }>
81
+ hotels: ExtractedHotelClaim[]
82
+ /** 渲染原语锚点(fact:<id>):命中行只走确定性回溯,启发式让位 */
83
+ anchors: Map<number, string>
71
84
  }
72
85
 
73
86
  /** 航班号:2 位承运码 + 3-4 位数字(UO784/EK328/MF1538/CZ8582/9C8781/FD597) */
@@ -94,6 +107,14 @@ function hasDirectAssertion(line: string): boolean {
94
107
 
95
108
  interface SectionCtx { origin?: string; destination?: string; date?: string }
96
109
 
110
+ /** 行内第一个命中词表的城市中文名(酒店 claim 的目的地键;无命中=undefined) */
111
+ function occ1(line: string, map: AirlineAirportMap): string | undefined {
112
+ for (const city of Object.keys(map.city_alias)) {
113
+ if (line.includes(city)) return city
114
+ }
115
+ return undefined
116
+ }
117
+
97
118
  /** 从文本(小节标题或行)解析航线上下文:箭头紧邻城市对(方向最可靠)优先,否则按出现位置取前两个;日期 M.D 或 MM-DD */
98
119
  function routeCtxOf(text: string, map: AirlineAirportMap, defaultYear?: number): SectionCtx {
99
120
  const occ: Array<{ city: string; iata: string; pos: number }> = []
@@ -140,12 +161,19 @@ function routeCtxOf(text: string, map: AirlineAirportMap, defaultYear?: number):
140
161
  * 行内自带上下文优先。默认年份由 trip window 提供(远期行程的年份不含糊)。
141
162
  */
142
163
  export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: { trip_year?: number }): ExtractedClaims {
143
- const claims: ExtractedClaims = { flights: [], policies: [], airports: [], direct_lines: [] }
164
+ const claims: ExtractedClaims = { flights: [], policies: [], airports: [], direct_lines: [], hotels: [], anchors: new Map() }
144
165
  const lines = markdown.split('\n')
145
166
  let section: SectionCtx = {}
167
+ // 渲染原语锚点(单向生成):带 fact:<id> 的行只走锚点确定性回溯,启发式抽取让位
168
+ const ANCHOR = /fact:([0-9a-f]{16})/
146
169
  for (let i = 0; i < lines.length; i++) {
147
170
  const line = lines[i]!
148
171
  const lineNo = i + 1
172
+ const anchorMatch = line.match(ANCHOR)
173
+ if (anchorMatch) {
174
+ claims.anchors.set(lineNo, anchorMatch[1]!.toLowerCase())
175
+ continue
176
+ }
149
177
  if (/^#{1,4}\s/.test(line)) {
150
178
  const ctx = routeCtxOf(line, map, opts?.trip_year)
151
179
  section = { origin: ctx.origin, destination: ctx.destination, date: ctx.date ?? section.date }
@@ -179,6 +207,19 @@ export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: {
179
207
  if (POLICY_WORD.test(line)) {
180
208
  claims.policies.push({ line: lineNo, text: line.trim().slice(0, 160), has_as_of: AS_OF_WORD.test(line) })
181
209
  }
210
+ // 酒店 claim(D-26):断言可住性(有房/可订/已核验/✓)的行——目的地与档期缺失时 fail-closed
211
+ const HOTEL_ASSERT = /酒店|住宿|客栈|民宿/
212
+ const HOTEL_BOOKABLE = /有房|可订|在架|已核验|已验证|可住|✓|✅/
213
+ if (HOTEL_ASSERT.test(line) && HOTEL_BOOKABLE.test(line)) {
214
+ const stays = [...line.matchAll(/(?<!\d)(\d{4}-\d{2}-\d{2})(?!\d)/g)].map(m => m[1]!)
215
+ claims.hotels.push({
216
+ line: lineNo,
217
+ text: line.trim().slice(0, 160),
218
+ destination: occ1(line, map),
219
+ check_in: stays[0],
220
+ check_out: stays[1],
221
+ })
222
+ }
182
223
  // 航司→机场映射 claim:同一行出现承运码 + 机场码,且该城是双机场映射面
183
224
  for (const [city, carriers] of Object.entries(map.carrier_airport)) {
184
225
  const airports = [...new Set(Object.values(carriers))]
@@ -207,6 +248,8 @@ export type GateViolationKind =
207
248
  | 'legs_inconsistent' // flight O&D 段与 legs 口径不一致
208
249
  | 'budget_floor_inconsistent' // 预算下限话术低于分项最低合计
209
250
  | 'date_order' // 住宿/段日期越窗或倒挂
251
+ | 'unverifiable_hotel_claim' // 酒店可住断言无 exact-date 事实回溯(D-26)
252
+ | 'fact_anchor_unknown' // 渲染锚点 fact:<id> 在注册表不存在(锚点被手改/伪造)
210
253
 
211
254
  export interface GateViolation {
212
255
  kind: GateViolationKind
@@ -242,6 +285,43 @@ export function gateArtifact(
242
285
  const violations: GateViolation[] = []
243
286
  let traceable = 0
244
287
 
288
+ // 渲染锚点优先(issue #118 单向生成):带 fact:<id> 的行确定性回溯注册表——
289
+ // 锚点在=按事实 bookability 判;锚点不存在=手改/伪造,直接违例。启发式对锚点行让位。
290
+ for (const [lineNo, factId] of claims.anchors) {
291
+ const f = facts.find(x => x.fact_id === factId)
292
+ if (!f) {
293
+ violations.push({ kind: 'fact_anchor_unknown', line: lineNo, detail: `渲染锚点 fact:${factId} 不在事实注册表——锚点被手改或伪造,产物不可信` })
294
+ continue
295
+ }
296
+ if (f.kind !== 'policy' && f.bookability === 'unavailable_exact_date') {
297
+ violations.push({ kind: 'not_in_source', line: lineNo, detail: `锚点事实为 exact-date 负事实(${(f as { fetched_at?: string }).fetched_at ?? ''})——负事实对应的可住/可订断言不得出现` })
298
+ continue
299
+ }
300
+ traceable++
301
+ }
302
+
303
+ // 酒店 claim(D-26):目的地+档期回溯酒店事实;无事实 fail-closed
304
+ for (const c of claims.hotels) {
305
+ if (claims.anchors.has(c.line)) continue
306
+ if (!c.destination) {
307
+ violations.push({ kind: 'unverifiable_hotel_claim', line: c.line, detail: `酒店可住断言缺目的地上下文,无法回溯 exact-date 事实——fail closed 按未核验处理` })
308
+ continue
309
+ }
310
+ const r = hotelClaimVerdict(facts, { destination: c.destination, check_in: c.check_in, check_out: c.check_out })
311
+ if (r.verdict === 'traceable') {
312
+ traceable++
313
+ continue
314
+ }
315
+ if (r.verdict === 'unverified') {
316
+ violations.push({ kind: 'unverifiable_hotel_claim', line: c.line, detail: r.reason })
317
+ } else {
318
+ violations.push({ kind: 'not_in_source', line: c.line, detail: r.reason })
319
+ }
320
+ if (CHECK_MARK.test(c.text)) {
321
+ violations.push({ kind: 'unconditional_check', line: c.line, detail: '对未核验酒店使用无条件 ✓/✅——只有 bookable_exact_date 才允许确定性标记' })
322
+ }
323
+ }
324
+
245
325
  for (const c of claims.flights) {
246
326
  if (c.carrier_only) {
247
327
  // 承运级直飞断言:注册表该 route+date 须存在该承运的 bookable 直飞
@@ -334,7 +414,7 @@ export function gateArtifact(
334
414
  if (!line.includes('联程')) return
335
415
  const nos = [...line.matchAll(FLIGHT_NO)].map(m => m[1]!.toUpperCase())
336
416
  const matched = facts.filter((f): f is Extract<BookableFact, { kind: 'flight' | 'train' }> =>
337
- f.kind !== 'policy' && nos.includes(f.flight_no.toUpperCase()))
417
+ (f.kind === 'flight' || f.kind === 'train') && nos.includes(f.flight_no.toUpperCase()))
338
418
  const allProtected = nos.length > 0
339
419
  && matched.length === nos.length
340
420
  && matched.every(f => f.protected_connection === true)
@@ -89,12 +89,19 @@ function invalidTerminal(): TerminalOutputValue {
89
89
  return { ok: false, error: 'invalid_terminal_output' }
90
90
  }
91
91
 
92
+ /** 剥离推理模型的思考标签对(如 MiniMax/DeepSeek 的 <think>…</think>):当代模型
93
+ * 即便被明令「只回终态包」也会以思考块前缀输出——不剥则严格终态门对推理模型
94
+ * 结构性失效(Round 9 治理面实测)。只剥配对的推理标签本体,其余前后缀仍 fail-closed。 */
95
+ function stripReasoningBlocks(raw: string): string {
96
+ return raw.replace(/<think>[\s\S]*?<\/think>\s*/gi, '')
97
+ }
98
+
92
99
  /** Parse exactly one configured tag pair containing one JSON object. */
93
100
  export function parseBenchmarkTerminal(raw: string, config: TerminalOutputConfig): TerminalOutputValue {
94
101
  if (!validateTerminalOutputConfig(config)) return invalidTerminal()
95
102
  if (Buffer.byteLength(raw, 'utf8') > config.max_bytes) return invalidTerminal()
96
103
 
97
- const trimmed = raw.trim()
104
+ const trimmed = stripReasoningBlocks(raw).trim()
98
105
  const opening = `<${config.tag}>`
99
106
  const closing = `</${config.tag}>`
100
107
  if (!trimmed.startsWith(opening) || !trimmed.endsWith(closing)) return invalidTerminal()
@@ -227,10 +234,11 @@ export function createBenchmarkAgentConformance(projection: BenchmarkBridgeProje
227
234
  }
228
235
  if (typeof event.data.callId !== 'string') return
229
236
  const args = parseToolArguments(event.data.arguments)
230
- const query = args && plainObject(args.query) ? args.query : undefined
231
- if (query?.action === 'call'
232
- && typeof query.tool === 'string'
233
- && projection.allowedTools.includes(query.tool)) {
237
+ // Round 8(issue #100/#102):bridge 工具 typed 泛化后,模型出参为平铺形态
238
+ const flat = args && plainObject(args) ? args : undefined
239
+ if (flat?.action === 'call'
240
+ && typeof flat.tool === 'string'
241
+ && projection.allowedTools.includes(flat.tool)) {
234
242
  state.validCallIds.add(event.data.callId)
235
243
  }
236
244
  return
@@ -338,7 +346,7 @@ function systemSection(projection: BenchmarkBridgeProjection): { name: string; t
338
346
  text: [
339
347
  'Benchmark execution contract:',
340
348
  `- Translate every task instruction to use a CLI, shell, Python, or agent_env.cli into the native tool ${projection.toolName}; do not merely describe the intended command.`,
341
- `- Call it with exactly {"query":{"action":"call","tool":"<one of: ${allowed}>","arguments":{...}}}.`,
349
+ `- Call it with exactly {"action":"call","tool":"<one of: ${allowed}>","arguments":{...}}.`,
342
350
  '- action:"tools" is discovery only and does not satisfy the required environment call.',
343
351
  `- After a successful tool result, reply only <${projection.terminal.tag}>{...one JSON object...}</${projection.terminal.tag}> with no prose or code fence.`,
344
352
  '- If a terminal-format correction arrives, reuse the existing result and do not call the tool again.',
@@ -244,6 +244,21 @@ function serializedArguments(value: Record<string, unknown>): { json: string; re
244
244
  }
245
245
  }
246
246
 
247
+ /** 可恢复 domain-error 契约(Round 8,issue #100/#102):封闭词表 + 逐码可恢复性与补救指引。
248
+ * 模型经 action=errors 拉取全表;失败返回的扁平形状保持不变(benchmark 诊断面依赖)。 */
249
+ export const BRIDGE_ERROR_CONTRACT: Record<string, { recoverable: boolean; remedy: string }> = Object.freeze({
250
+ invalid_action: { recoverable: true, remedy: 'action 只允许 tools 或 call' },
251
+ disallowed_tool: { recoverable: true, remedy: '先 action=tools 列出 allowed_tools 清单,再从清单内选工具' },
252
+ invalid_arguments: { recoverable: true, remedy: 'arguments 必须是可序列化 JSON 对象(≤64KB、深度≤12);收窄后重试' },
253
+ timed_out: { recoverable: true, remedy: '上游超时;可原样重试一次或换其他工具' },
254
+ output_truncated: { recoverable: true, remedy: '输出超出上限;让被调工具收窄查询范围后重试' },
255
+ invalid_json: { recoverable: true, remedy: '上游输出不是合法 JSON;重试一次或换工具' },
256
+ invalid_output: { recoverable: true, remedy: '上游输出结构不符(如原始字符串);重试或换工具' },
257
+ runner_failed: { recoverable: true, remedy: '上游进程非零退出;可重试一次,持续失败换工具' },
258
+ spawn_failed: { recoverable: false, remedy: '可执行文件不可用——环境问题,调用方不可恢复' },
259
+ forbidden_output: { recoverable: false, remedy: '输出含未授权键(策略边界)——不可恢复,换工具或放弃' },
260
+ })
261
+
247
262
  /** Register the opt-in model-facing bridge. */
248
263
  export function registerBenchmarkEnvironmentBridge(
249
264
  path: string,
@@ -256,22 +271,26 @@ export function registerBenchmarkEnvironmentBridge(
256
271
 
257
272
  register(defineTool({
258
273
  name: 'gotry_benchmark_environment',
259
- description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed.',
274
+ description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed. '
275
+ + 'Recoverable domain-error contract: every failure returns { ok:false, error:<code>, ... } from a closed vocabulary; '
276
+ + 'call action=errors to fetch the per-code recoverable flag and remedy before retrying.',
277
+ // Round 8(issue #100/#102):generic typed schema——模型可见逐字段契约(与产品面 D-30 同刀法);
278
+ // arguments 保持 additionalProperties:true(被调工具的参数面由其自身契约定义)
260
279
  parameters: {
261
- query: {
262
- type: 'json',
263
- required: true,
264
- description: '{ action: "tools" | "call", tool?: string, arguments?: object }',
265
- },
280
+ action: { type: 'string', enum: ['tools', 'call', 'errors'], required: true, description: 'tools=列出可调工具;call=执行被映射工具;errors=拉取可恢复错误契约表' },
281
+ tool: { type: 'string', description: '被调工具名(必须在 allowed_tools 清单内;action=call 时必填)' },
282
+ arguments: { type: 'object', additionalProperties: true, description: '传给被调工具的参数对象(action=call 时可选;≤64KB、深度≤12)' },
266
283
  },
267
284
  output: {
268
285
  schema: { type: 'json' },
269
286
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
270
287
  },
271
- async execute(args: { query?: unknown }): Promise<Record<string, never>> {
272
- const query = plainObject(args?.query) ? args.query : {}
273
- if (query.action === 'tools') return jsonObject({ ok: true, tools: bridge.allowed_tools })
274
- if (query.action !== 'call') return jsonObject({ ok: false, error: 'invalid_action' })
288
+ async execute(args): Promise<Record<string, never>> {
289
+ const action = args?.action
290
+ if (action === 'errors') return jsonObject({ ok: true, errors: BRIDGE_ERROR_CONTRACT })
291
+ if (action === 'tools') return jsonObject({ ok: true, tools: bridge.allowed_tools })
292
+ if (action !== 'call') return jsonObject({ ok: false, error: 'invalid_action' })
293
+ const query = args as unknown as { tool?: unknown; arguments?: unknown }
275
294
  if (typeof query.tool !== 'string' || !bridge.allowed_tools.includes(query.tool)) {
276
295
  return jsonObject({ ok: false, error: 'disallowed_tool' })
277
296
  }
@@ -89,7 +89,34 @@ export interface PolicyFact {
89
89
  review_by?: string
90
90
  }
91
91
 
92
- export type BookableFact = FlightFact | PolicyFact
92
+ export type BookableFact = FlightFact | PolicyFact | HotelFact
93
+
94
+ /**
95
+ * 酒店事实(D-26,issue #118):exact-date 酒店检索(hit/miss)的唯一事实形态。
96
+ * 纪律与机/火同源:①未定档期的摸底检索不落账(无 exact-date 语义);
97
+ * ②needs-setup/error 是传输失败,永不落负事实;③价格上游打码(¥7xx)——
98
+ * 本事实**不落数字价**,只记在架家数,真实价以落地页为准(打码价保真纪律)。
99
+ */
100
+ export interface HotelFact {
101
+ schema: typeof BOOKABLE_FACT_SCHEMA
102
+ /** 语义派生 id:sha(destination|checkIn|checkOut|query_id)——同查询重放幂等 */
103
+ fact_id: string
104
+ kind: 'hotel'
105
+ /** 目的地中文(flyai/session 的 to;闸侧 claim 匹配键) */
106
+ destination: string
107
+ check_in?: string // YYYY-MM-DD
108
+ check_out?: string // YYYY-MM-DD
109
+ verdict: 'hit' | 'miss'
110
+ bookability: Bookability
111
+ /** 在架条数(hit 时;价格打码不落数字价) */
112
+ options_masked: number
113
+ source: string // flyai-hotel / session:ctrip-hotel
114
+ /** 可重放查询 id(如 flyai:hotel:大理:2026-10-01_2026-10-03) */
115
+ query_id: string
116
+ fetched_at: string
117
+ as_of: string
118
+ tier: EvidenceTier
119
+ }
93
120
 
94
121
  // ---------------------------------------------------------------------------
95
122
  // id 与查询词(可重放:query_id 即重放凭证)
@@ -133,7 +160,8 @@ export interface FlyaiLikeOption {
133
160
  }
134
161
 
135
162
  export interface FlyaiLikeResult {
136
- verdict: 'hit' | 'miss' | 'error'
163
+ /** needs-setup(试用额度达限)同 error:无结论不是证据,不落事实 */
164
+ verdict: 'hit' | 'miss' | 'error' | 'needs-setup'
137
165
  options?: FlyaiLikeOption[]
138
166
  evidence?: string
139
167
  }
@@ -190,7 +218,7 @@ export function factsFromFlyai(q: { kind: 'flight' | 'train'; origin: string; de
190
218
  const queryId = makeQueryId(source, q.kind, q.origin, q.destination, q.date)
191
219
  const origin = iataOf(alias, q.origin)
192
220
  const destination = iataOf(alias, q.destination)
193
- if (r.verdict === 'error') return []
221
+ if (r.verdict === 'error' || r.verdict === 'needs-setup') return []
194
222
  if (r.verdict === 'miss') return [negativeFact(queryId, q.kind, q.origin, q.destination, q.date, source, fetchedAt, alias)]
195
223
  const facts: FlightFact[] = []
196
224
  for (const o of r.options ?? []) {
@@ -278,7 +306,7 @@ export function dedupeFacts<T extends BookableFact>(facts: T[]): T[] {
278
306
  /** route+date 的最新查询结论(同日多次查询:最新一次为准,避免「昨天 miss 今天 hit」陈旧否定) */
279
307
  export function latestFactsForRouteDate(facts: BookableFact[], origin: string, destination: string, date: string): FlightFact[] {
280
308
  const hits = facts.filter((f): f is FlightFact =>
281
- f.kind !== 'policy'
309
+ (f.kind === 'flight' || f.kind === 'train')
282
310
  && f.route.origin === origin && f.route.destination === destination && f.date === date)
283
311
  const latestQueryTs = new Map<string, string>()
284
312
  for (const f of hits) {
@@ -310,7 +338,7 @@ export interface FlightClaim {
310
338
  export function flightClaimVerdict(facts: BookableFact[], claim: FlightClaim): { verdict: FlightClaimVerdict; fact?: FlightFact; reason: string } {
311
339
  const no = claim.flight_no.toUpperCase().replace(/\s+/g, '')
312
340
  const sameNo = facts.filter((f): f is FlightFact =>
313
- f.kind !== 'policy' && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date')
341
+ (f.kind === 'flight' || f.kind === 'train') && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date')
314
342
  const routeMatched = sameNo.filter(f =>
315
343
  (!claim.origin || f.route.origin === claim.origin || f.route.origin_airport === claim.origin)
316
344
  && (!claim.destination || f.route.destination === claim.destination || f.route.dest_airport === claim.destination)
@@ -502,6 +530,12 @@ export function defaultReviewBy(tripDate: string): string {
502
530
  * 永不输出无条件 ✓;证据链(source/query_id/fetched_at)随行必达。
503
531
  */
504
532
  export function renderFlightFact(f: FlightFact): string {
533
+ return renderFlightFactInner(f) + ` <!-- fact:${f.fact_id} -->`
534
+ }
535
+
536
+ function renderFlightFactInner(f: FlightFact): string {
537
+ // 单向生成锚点(D-26):每条事实行内嵌 fact_id,闸侧锚点匹配 = 确定性回溯
538
+ const anchor = ` <!-- fact:${f.fact_id} -->`
505
539
  const route = `${f.route.origin}→${f.route.destination}`
506
540
  const evidence = `[${f.source}@${f.fetched_at} #${f.query_id}]`
507
541
  if (f.bookability === 'unavailable_exact_date') {
@@ -555,3 +589,73 @@ export function renderNightsLine(it: ItineraryFacts): string {
555
589
  const flightLegs = it.od_segments.filter(s => s.mode === 'flight').reduce((acc, s) => acc + s.legs, 0)
556
590
  return `共 ${daysBetween(it.trip_start, it.trip_end) + 1} 天 ${totalNights} 晚 = ${hotelNights} 个酒店夜 + ${it.onboard_nights} 个机上夜;航班按 O&D ${flightOd} 段(共 ${flightLegs} 个 flight legs)`
557
591
  }
592
+
593
+ // ---------------------------------------------------------------------------
594
+ // 酒店事实(D-26,issue #118):exact-date 酒店检索 hit/miss 落账
595
+ // 纪律:摸底(未定档期)不落账(无 exact-date 语义);needs-setup/error 传输失败永不落负事实
596
+ // ---------------------------------------------------------------------------
597
+
598
+ export function factsFromHotel(input: {
599
+ source: string
600
+ destination: string
601
+ checkIn?: string
602
+ checkOut?: string
603
+ verdict: 'hit' | 'miss' | 'needs-setup' | 'error'
604
+ options: number
605
+ evidence: string
606
+ fetchedAt: string
607
+ }): HotelFact[] {
608
+ if (!input.checkIn || !input.checkOut) return []
609
+ if (input.verdict !== 'hit' && input.verdict !== 'miss') return []
610
+ const queryId = `hotel:${input.source}:${input.destination}:${input.checkIn}_${input.checkOut}`
611
+ return [{
612
+ schema: BOOKABLE_FACT_SCHEMA,
613
+ kind: 'hotel',
614
+ fact_id: makeFactId([input.destination, input.checkIn, input.checkOut, queryId]),
615
+ destination: input.destination,
616
+ check_in: input.checkIn,
617
+ check_out: input.checkOut,
618
+ verdict: input.verdict,
619
+ bookability: input.verdict === 'hit' ? 'bookable_exact_date' : 'unavailable_exact_date',
620
+ options_masked: input.options,
621
+ source: input.source,
622
+ query_id: queryId,
623
+ fetched_at: input.fetchedAt,
624
+ as_of: input.fetchedAt.slice(0, 10),
625
+ tier: 'live_inventory',
626
+ }]
627
+ }
628
+
629
+ /** 酒店 claim 回溯:同目的地+同档期的 exact-date 事实裁决(无事实=unverified,fail-closed) */
630
+ export function hotelClaimVerdict(
631
+ facts: BookableFact[],
632
+ claim: { destination: string; check_in?: string; check_out?: string },
633
+ ): { verdict: 'traceable' | 'not_in_source' | 'unverified'; fact?: HotelFact; reason: string } {
634
+ const scoped = facts.filter((f): f is HotelFact =>
635
+ f.kind === 'hotel' && f.destination === claim.destination
636
+ && (!claim.check_in || f.check_in === claim.check_in)
637
+ && (!claim.check_out || f.check_out === claim.check_out))
638
+ if (scoped.length === 0) {
639
+ return {
640
+ verdict: 'unverified',
641
+ reason: `酒店 claim(${claim.destination}${claim.check_in ? ` ${claim.check_in}→${claim.check_out ?? '?'}` : ''})未经 exact-date 源核验——先检索再写结论`,
642
+ }
643
+ }
644
+ const hit = scoped.find(f => f.bookability === 'bookable_exact_date')
645
+ if (hit) return { verdict: 'traceable', fact: hit, reason: 'exact-date 命中' }
646
+ const latest = scoped[scoped.length - 1]!
647
+ return { verdict: 'not_in_source', fact: latest, reason: `该档期 exact-date 源正常返回 0 条(${latest.fetched_at})——不得写有房/可订` }
648
+ }
649
+
650
+ // ---------------------------------------------------------------------------
651
+ // 渲染原语(单向生成,issue #118):事实 → 行,锚点内嵌 fact_id;
652
+ // 产物经原语渲染即自带溯源锚,闸侧锚点匹配 = 确定性回溯,不靠行文猜测
653
+ // ---------------------------------------------------------------------------
654
+
655
+
656
+ export function renderHotelFact(f: HotelFact): string {
657
+ if (f.verdict !== 'hit') {
658
+ return `- ${f.destination} ${f.check_in}→${f.check_out}:exact-date 源 0 条在架(不可订) <!-- fact:${f.fact_id} -->`
659
+ }
660
+ return `- ${f.destination} ${f.check_in}→${f.check_out}:住宿已核验(在架 ${f.options_masked} 家;价格上游打码,真实价以落地页为准) <!-- fact:${f.fact_id} -->`
661
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 预订 saga 状态机(booking_saga_fsm.v1)——issue #17 采纳三点的具名化落点。
3
3
  *
4
- * 立场(docs/booking-saga-fsm.md / ADR-17):不引入编排框架;预订 saga 的状态字母表与边表
4
+ * 立场(docs/design/booking-saga-fsm.md / ADR-17):不引入编排框架;预订 saga 的状态字母表与边表
5
5
  * 就是账本 pending_writes(status CHECK:pending|confirmed|compensated)的显式化。
6
6
  * 本模块是纯函数词汇层,零依赖、零行为、不接线任何 seam——M5 拍板 WriteGate 生产化时
7
7
  * 以此为唯一状态推进词汇(具名化 = 状态与边不再散落在 SQL 字符串里)。