@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
@@ -1,4 +1,4 @@
1
- import { flightClaimVerdict, itineraryInvariants, latestFactsForRouteDate } from './bookable-facts.js';
1
+ import { flightClaimVerdict, hotelClaimVerdict, itineraryInvariants, latestFactsForRouteDate } from './bookable-facts.js';
2
2
  export const ARTIFACT_GATE_SCHEMA = 'gotry_artifact_gate.v1';
3
3
  const FLIGHT_NO = /(?<![A-Za-z0-9])([A-Z0-9]{2}\d{3,4})(?![\d])/g;
4
4
  const CARRIER_DIRECT = /(?<![A-Za-z0-9])([A-Z0-9]{2})(?!\d)(?:\s|[一-龥]){0,6}直飞/g;
@@ -26,6 +26,12 @@ function hasDirectAssertion(line) {
26
26
  const before = line.slice(Math.max(0, idx - 3), idx);
27
27
  return !(before.endsWith('无') || before.endsWith('不') || before.endsWith('没有') || before.endsWith('未见'));
28
28
  }
29
+ function occ1(line, map) {
30
+ for (const city of Object.keys(map.city_alias)){
31
+ if (line.includes(city)) return city;
32
+ }
33
+ return undefined;
34
+ }
29
35
  function routeCtxOf(text, map, defaultYear) {
30
36
  const occ = [];
31
37
  for (const [city, iata] of Object.entries(map.city_alias)){
@@ -72,13 +78,21 @@ export function extractClaims(markdown, map, opts) {
72
78
  flights: [],
73
79
  policies: [],
74
80
  airports: [],
75
- direct_lines: []
81
+ direct_lines: [],
82
+ hotels: [],
83
+ anchors: new Map()
76
84
  };
77
85
  const lines = markdown.split('\n');
78
86
  let section = {};
87
+ const ANCHOR = /fact:([0-9a-f]{16})/;
79
88
  for(let i = 0; i < lines.length; i++){
80
89
  const line = lines[i];
81
90
  const lineNo = i + 1;
91
+ const anchorMatch = line.match(ANCHOR);
92
+ if (anchorMatch) {
93
+ claims.anchors.set(lineNo, anchorMatch[1].toLowerCase());
94
+ continue;
95
+ }
82
96
  if (/^#{1,4}\s/.test(line)) {
83
97
  const ctx = routeCtxOf(line, map, opts?.trip_year);
84
98
  section = {
@@ -145,6 +159,20 @@ export function extractClaims(markdown, map, opts) {
145
159
  has_as_of: AS_OF_WORD.test(line)
146
160
  });
147
161
  }
162
+ const HOTEL_ASSERT = /酒店|住宿|客栈|民宿/;
163
+ const HOTEL_BOOKABLE = /有房|可订|在架|已核验|已验证|可住|✓|✅/;
164
+ if (HOTEL_ASSERT.test(line) && HOTEL_BOOKABLE.test(line)) {
165
+ const stays = [
166
+ ...line.matchAll(/(?<!\d)(\d{4}-\d{2}-\d{2})(?!\d)/g)
167
+ ].map((m)=>m[1]);
168
+ claims.hotels.push({
169
+ line: lineNo,
170
+ text: line.trim().slice(0, 160),
171
+ destination: occ1(line, map),
172
+ check_in: stays[0],
173
+ check_out: stays[1]
174
+ });
175
+ }
148
176
  for (const [city, carriers] of Object.entries(map.carrier_airport)){
149
177
  const airports = [
150
178
  ...new Set(Object.values(carriers))
@@ -176,6 +204,66 @@ export function gateArtifact(markdown, facts, map, opts) {
176
204
  const claims = extractClaims(markdown, map, opts);
177
205
  const violations = [];
178
206
  let traceable = 0;
207
+ for (const [lineNo, factId] of claims.anchors){
208
+ const f = facts.find((x)=>x.fact_id === factId);
209
+ if (!f) {
210
+ violations.push({
211
+ kind: 'fact_anchor_unknown',
212
+ line: lineNo,
213
+ detail: `渲染锚点 fact:${factId} 不在事实注册表——锚点被手改或伪造,产物不可信`
214
+ });
215
+ continue;
216
+ }
217
+ if (f.kind !== 'policy' && f.bookability === 'unavailable_exact_date') {
218
+ violations.push({
219
+ kind: 'not_in_source',
220
+ line: lineNo,
221
+ detail: `锚点事实为 exact-date 负事实(${f.fetched_at ?? ''})——负事实对应的可住/可订断言不得出现`
222
+ });
223
+ continue;
224
+ }
225
+ traceable++;
226
+ }
227
+ for (const c of claims.hotels){
228
+ if (claims.anchors.has(c.line)) continue;
229
+ if (!c.destination) {
230
+ violations.push({
231
+ kind: 'unverifiable_hotel_claim',
232
+ line: c.line,
233
+ detail: `酒店可住断言缺目的地上下文,无法回溯 exact-date 事实——fail closed 按未核验处理`
234
+ });
235
+ continue;
236
+ }
237
+ const r = hotelClaimVerdict(facts, {
238
+ destination: c.destination,
239
+ check_in: c.check_in,
240
+ check_out: c.check_out
241
+ });
242
+ if (r.verdict === 'traceable') {
243
+ traceable++;
244
+ continue;
245
+ }
246
+ if (r.verdict === 'unverified') {
247
+ violations.push({
248
+ kind: 'unverifiable_hotel_claim',
249
+ line: c.line,
250
+ detail: r.reason
251
+ });
252
+ } else {
253
+ violations.push({
254
+ kind: 'not_in_source',
255
+ line: c.line,
256
+ detail: r.reason
257
+ });
258
+ }
259
+ if (CHECK_MARK.test(c.text)) {
260
+ violations.push({
261
+ kind: 'unconditional_check',
262
+ line: c.line,
263
+ detail: '对未核验酒店使用无条件 ✓/✅——只有 bookable_exact_date 才允许确定性标记'
264
+ });
265
+ }
266
+ }
179
267
  for (const c of claims.flights){
180
268
  if (c.carrier_only) {
181
269
  const hasCarrier = facts.some((f)=>f.kind === 'flight' && f.bookability === 'bookable_exact_date' && f.nonstop !== false && f.flight_no.startsWith(c.carrier_only) && (!c.origin || f.route.origin === c.origin || f.route.origin_airport === c.origin) && (!c.destination || f.route.destination === c.destination || f.route.dest_airport === c.destination) && (!c.date || f.date === c.date));
@@ -277,7 +365,7 @@ export function gateArtifact(markdown, facts, map, opts) {
277
365
  const nos = [
278
366
  ...line.matchAll(FLIGHT_NO)
279
367
  ].map((m)=>m[1].toUpperCase());
280
- const matched = facts.filter((f)=>f.kind !== 'policy' && nos.includes(f.flight_no.toUpperCase()));
368
+ const matched = facts.filter((f)=>(f.kind === 'flight' || f.kind === 'train') && nos.includes(f.flight_no.toUpperCase()));
281
369
  const allProtected = nos.length > 0 && matched.length === nos.length && matched.every((f)=>f.protected_connection === true);
282
370
  if (!allProtected) {
283
371
  violations.push({
@@ -29,10 +29,13 @@ function invalidTerminal() {
29
29
  error: 'invalid_terminal_output'
30
30
  };
31
31
  }
32
+ function stripReasoningBlocks(raw) {
33
+ return raw.replace(/<think>[\s\S]*?<\/think>\s*/gi, '');
34
+ }
32
35
  export function parseBenchmarkTerminal(raw, config) {
33
36
  if (!validateTerminalOutputConfig(config)) return invalidTerminal();
34
37
  if (Buffer.byteLength(raw, 'utf8') > config.max_bytes) return invalidTerminal();
35
- const trimmed = raw.trim();
38
+ const trimmed = stripReasoningBlocks(raw).trim();
36
39
  const opening = `<${config.tag}>`;
37
40
  const closing = `</${config.tag}>`;
38
41
  if (!trimmed.startsWith(opening) || !trimmed.endsWith(closing)) return invalidTerminal();
@@ -150,8 +153,8 @@ export function createBenchmarkAgentConformance(projection) {
150
153
  }
151
154
  if (typeof event.data.callId !== 'string') return;
152
155
  const args = parseToolArguments(event.data.arguments);
153
- const query = args && plainObject(args.query) ? args.query : undefined;
154
- if (query?.action === 'call' && typeof query.tool === 'string' && projection.allowedTools.includes(query.tool)) {
156
+ const flat = args && plainObject(args) ? args : undefined;
157
+ if (flat?.action === 'call' && typeof flat.tool === 'string' && projection.allowedTools.includes(flat.tool)) {
155
158
  state.validCallIds.add(event.data.callId);
156
159
  }
157
160
  return;
@@ -261,7 +264,7 @@ function systemSection(projection) {
261
264
  text: [
262
265
  'Benchmark execution contract:',
263
266
  `- 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.`,
264
- `- Call it with exactly {"query":{"action":"call","tool":"<one of: ${allowed}>","arguments":{...}}}.`,
267
+ `- Call it with exactly {"action":"call","tool":"<one of: ${allowed}>","arguments":{...}}.`,
265
268
  '- action:"tools" is discovery only and does not satisfy the required environment call.',
266
269
  `- After a successful tool result, reply only <${projection.terminal.tag}>{...one JSON object...}</${projection.terminal.tag}> with no prose or code fence.`,
267
270
  '- If a terminal-format correction arrives, reuse the existing result and do not call the tool again.'
@@ -211,18 +211,74 @@ function serializedArguments(value) {
211
211
  };
212
212
  }
213
213
  }
214
+ export const BRIDGE_ERROR_CONTRACT = Object.freeze({
215
+ invalid_action: {
216
+ recoverable: true,
217
+ remedy: 'action 只允许 tools 或 call'
218
+ },
219
+ disallowed_tool: {
220
+ recoverable: true,
221
+ remedy: '先 action=tools 列出 allowed_tools 清单,再从清单内选工具'
222
+ },
223
+ invalid_arguments: {
224
+ recoverable: true,
225
+ remedy: 'arguments 必须是可序列化 JSON 对象(≤64KB、深度≤12);收窄后重试'
226
+ },
227
+ timed_out: {
228
+ recoverable: true,
229
+ remedy: '上游超时;可原样重试一次或换其他工具'
230
+ },
231
+ output_truncated: {
232
+ recoverable: true,
233
+ remedy: '输出超出上限;让被调工具收窄查询范围后重试'
234
+ },
235
+ invalid_json: {
236
+ recoverable: true,
237
+ remedy: '上游输出不是合法 JSON;重试一次或换工具'
238
+ },
239
+ invalid_output: {
240
+ recoverable: true,
241
+ remedy: '上游输出结构不符(如原始字符串);重试或换工具'
242
+ },
243
+ runner_failed: {
244
+ recoverable: true,
245
+ remedy: '上游进程非零退出;可重试一次,持续失败换工具'
246
+ },
247
+ spawn_failed: {
248
+ recoverable: false,
249
+ remedy: '可执行文件不可用——环境问题,调用方不可恢复'
250
+ },
251
+ forbidden_output: {
252
+ recoverable: false,
253
+ remedy: '输出含未授权键(策略边界)——不可恢复,换工具或放弃'
254
+ }
255
+ });
214
256
  export function registerBenchmarkEnvironmentBridge(path, register, subprocess) {
215
257
  const bridge = loadBenchmarkEnvironmentConfig(path);
216
258
  if (!bridge) throw new Error('benchmark environment bridge configuration unavailable');
217
259
  if (!subprocess) throw new Error('benchmark environment bridge subprocess unavailable');
218
260
  register(defineTool({
219
261
  name: 'gotry_benchmark_environment',
220
- description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed.',
262
+ description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed. ' + 'Recoverable domain-error contract: every failure returns { ok:false, error:<code>, ... } from a closed vocabulary; ' + 'call action=errors to fetch the per-code recoverable flag and remedy before retrying.',
221
263
  parameters: {
222
- query: {
223
- type: 'json',
264
+ action: {
265
+ type: 'string',
266
+ enum: [
267
+ 'tools',
268
+ 'call',
269
+ 'errors'
270
+ ],
224
271
  required: true,
225
- description: '{ action: "tools" | "call", tool?: string, arguments?: object }'
272
+ description: 'tools=列出可调工具;call=执行被映射工具;errors=拉取可恢复错误契约表'
273
+ },
274
+ tool: {
275
+ type: 'string',
276
+ description: '被调工具名(必须在 allowed_tools 清单内;action=call 时必填)'
277
+ },
278
+ arguments: {
279
+ type: 'object',
280
+ additionalProperties: true,
281
+ description: '传给被调工具的参数对象(action=call 时可选;≤64KB、深度≤12)'
226
282
  }
227
283
  },
228
284
  output: {
@@ -237,15 +293,20 @@ export function registerBenchmarkEnvironmentBridge(path, register, subprocess) {
237
293
  ]
238
294
  },
239
295
  async execute (args) {
240
- const query = plainObject(args?.query) ? args.query : {};
241
- if (query.action === 'tools') return jsonObject({
296
+ const action = args?.action;
297
+ if (action === 'errors') return jsonObject({
298
+ ok: true,
299
+ errors: BRIDGE_ERROR_CONTRACT
300
+ });
301
+ if (action === 'tools') return jsonObject({
242
302
  ok: true,
243
303
  tools: bridge.allowed_tools
244
304
  });
245
- if (query.action !== 'call') return jsonObject({
305
+ if (action !== 'call') return jsonObject({
246
306
  ok: false,
247
307
  error: 'invalid_action'
248
308
  });
309
+ const query = args;
249
310
  if (typeof query.tool !== 'string' || !bridge.allowed_tools.includes(query.tool)) {
250
311
  return jsonObject({
251
312
  ok: false,
@@ -52,7 +52,7 @@ export function factsFromFlyai(q, r, fetchedAt, alias) {
52
52
  const queryId = makeQueryId(source, q.kind, q.origin, q.destination, q.date);
53
53
  const origin = iataOf(alias, q.origin);
54
54
  const destination = iataOf(alias, q.destination);
55
- if (r.verdict === 'error') return [];
55
+ if (r.verdict === 'error' || r.verdict === 'needs-setup') return [];
56
56
  if (r.verdict === 'miss') return [
57
57
  negativeFact(queryId, q.kind, q.origin, q.destination, q.date, source, fetchedAt, alias)
58
58
  ];
@@ -159,7 +159,7 @@ export function dedupeFacts(facts) {
159
159
  ].sort((a, b)=>a.fetched_at.localeCompare(b.fetched_at) || a.fact_id.localeCompare(b.fact_id));
160
160
  }
161
161
  export function latestFactsForRouteDate(facts, origin, destination, date) {
162
- const hits = facts.filter((f)=>f.kind !== 'policy' && f.route.origin === origin && f.route.destination === destination && f.date === date);
162
+ const hits = facts.filter((f)=>(f.kind === 'flight' || f.kind === 'train') && f.route.origin === origin && f.route.destination === destination && f.date === date);
163
163
  const latestQueryTs = new Map();
164
164
  for (const f of hits){
165
165
  const prev = latestQueryTs.get(f.query_id);
@@ -173,7 +173,7 @@ export function latestFactsForRouteDate(facts, origin, destination, date) {
173
173
  }
174
174
  export function flightClaimVerdict(facts, claim) {
175
175
  const no = claim.flight_no.toUpperCase().replace(/\s+/g, '');
176
- const sameNo = facts.filter((f)=>f.kind !== 'policy' && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date');
176
+ const sameNo = facts.filter((f)=>(f.kind === 'flight' || f.kind === 'train') && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date');
177
177
  const routeMatched = sameNo.filter((f)=>(!claim.origin || f.route.origin === claim.origin || f.route.origin_airport === claim.origin) && (!claim.destination || f.route.destination === claim.destination || f.route.dest_airport === claim.destination) && (!claim.date || f.date === claim.date));
178
178
  if (routeMatched.length > 0) return {
179
179
  verdict: 'traceable',
@@ -310,6 +310,10 @@ export function defaultReviewBy(tripDate) {
310
310
  return d.toISOString().slice(0, 10);
311
311
  }
312
312
  export function renderFlightFact(f) {
313
+ return renderFlightFactInner(f) + ` <!-- fact:${f.fact_id} -->`;
314
+ }
315
+ function renderFlightFactInner(f) {
316
+ const anchor = ` <!-- fact:${f.fact_id} -->`;
313
317
  const route = `${f.route.origin}→${f.route.destination}`;
314
318
  const evidence = `[${f.source}@${f.fetched_at} #${f.query_id}]`;
315
319
  if (f.bookability === 'unavailable_exact_date') {
@@ -344,6 +348,61 @@ export function renderNightsLine(it) {
344
348
  const flightLegs = it.od_segments.filter((s)=>s.mode === 'flight').reduce((acc, s)=>acc + s.legs, 0);
345
349
  return `共 ${daysBetween(it.trip_start, it.trip_end) + 1} 天 ${totalNights} 晚 = ${hotelNights} 个酒店夜 + ${it.onboard_nights} 个机上夜;航班按 O&D ${flightOd} 段(共 ${flightLegs} 个 flight legs)`;
346
350
  }
351
+ export function factsFromHotel(input) {
352
+ if (!input.checkIn || !input.checkOut) return [];
353
+ if (input.verdict !== 'hit' && input.verdict !== 'miss') return [];
354
+ const queryId = `hotel:${input.source}:${input.destination}:${input.checkIn}_${input.checkOut}`;
355
+ return [
356
+ {
357
+ schema: BOOKABLE_FACT_SCHEMA,
358
+ kind: 'hotel',
359
+ fact_id: makeFactId([
360
+ input.destination,
361
+ input.checkIn,
362
+ input.checkOut,
363
+ queryId
364
+ ]),
365
+ destination: input.destination,
366
+ check_in: input.checkIn,
367
+ check_out: input.checkOut,
368
+ verdict: input.verdict,
369
+ bookability: input.verdict === 'hit' ? 'bookable_exact_date' : 'unavailable_exact_date',
370
+ options_masked: input.options,
371
+ source: input.source,
372
+ query_id: queryId,
373
+ fetched_at: input.fetchedAt,
374
+ as_of: input.fetchedAt.slice(0, 10),
375
+ tier: 'live_inventory'
376
+ }
377
+ ];
378
+ }
379
+ export function hotelClaimVerdict(facts, claim) {
380
+ const scoped = facts.filter((f)=>f.kind === 'hotel' && f.destination === claim.destination && (!claim.check_in || f.check_in === claim.check_in) && (!claim.check_out || f.check_out === claim.check_out));
381
+ if (scoped.length === 0) {
382
+ return {
383
+ verdict: 'unverified',
384
+ reason: `酒店 claim(${claim.destination}${claim.check_in ? ` ${claim.check_in}→${claim.check_out ?? '?'}` : ''})未经 exact-date 源核验——先检索再写结论`
385
+ };
386
+ }
387
+ const hit = scoped.find((f)=>f.bookability === 'bookable_exact_date');
388
+ if (hit) return {
389
+ verdict: 'traceable',
390
+ fact: hit,
391
+ reason: 'exact-date 命中'
392
+ };
393
+ const latest = scoped[scoped.length - 1];
394
+ return {
395
+ verdict: 'not_in_source',
396
+ fact: latest,
397
+ reason: `该档期 exact-date 源正常返回 0 条(${latest.fetched_at})——不得写有房/可订`
398
+ };
399
+ }
400
+ export function renderHotelFact(f) {
401
+ if (f.verdict !== 'hit') {
402
+ return `- ${f.destination} ${f.check_in}→${f.check_out}:exact-date 源 0 条在架(不可订) <!-- fact:${f.fact_id} -->`;
403
+ }
404
+ return `- ${f.destination} ${f.check_in}→${f.check_out}:住宿已核验(在架 ${f.options_masked} 家;价格上游打码,真实价以落地页为准) <!-- fact:${f.fact_id} -->`;
405
+ }
347
406
 
348
407
 
349
408
  //# sourceURL=ts/src/bookable-facts.ts