@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.20
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.
- package/README.md +42 -16
- package/README.zh-CN.md +37 -15
- package/bin/gotry-bootstrap.js +309 -30
- package/bin/gotry-inner.js +74 -23
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +22 -8
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/anything.js +62 -29
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +307 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/hbcli.js +28 -2
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/anything-tests.js +70 -21
- package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
- package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +113 -2
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +175 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/hbcli-tests.js +38 -1
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +39 -16
- package/dist/src/benchmark-environment-bridge.js +316 -52
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +739 -150
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +7 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +89 -38
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/hbcli.ts +53 -4
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/dsh-runtime/vendor/README.md +60 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
- package/ts/package.json +9 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +54 -15
- package/ts/src/benchmark-environment-bridge.ts +234 -46
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +492 -172
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +365 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- package/ts/src/tool-budget.ts +0 -165
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { accessSync, constants, lstatSync, readFileSync, statSync } from 'node:fs'
|
|
2
2
|
import { getuid } from 'node:process'
|
|
3
3
|
import { isAbsolute } from 'node:path'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
assertSupportedJsonSchema,
|
|
6
|
+
validateJsonSchemaValue,
|
|
7
|
+
ToolArgsError,
|
|
8
|
+
type JsonSchemaNode,
|
|
9
|
+
type ObjectJsonSchema,
|
|
10
|
+
type ToolDefinition,
|
|
11
|
+
} from '@deepseek-ai/dsh-tools'
|
|
5
12
|
import {
|
|
6
13
|
validateTerminalOutputConfig,
|
|
7
14
|
type BenchmarkBridgeProjection,
|
|
8
15
|
type TerminalOutputConfig,
|
|
9
16
|
} from './benchmark-agent-conformance.ts'
|
|
10
17
|
|
|
11
|
-
const SCHEMA_VERSION = '
|
|
18
|
+
const SCHEMA_VERSION = 'gotry_benchmark_environment_bridge_v3'
|
|
19
|
+
export const BENCHMARK_TOOL_RESULT_SCHEMA_VERSION = 'gotry_benchmark_tool_result_v1'
|
|
20
|
+
export const BENCHMARK_DOMAIN_RECOVERIES = ['none', 'retry_same', 'revise_arguments', 'choose_alternative'] as const
|
|
21
|
+
type BenchmarkDomainRecovery = typeof BENCHMARK_DOMAIN_RECOVERIES[number]
|
|
12
22
|
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]*$/
|
|
13
|
-
const
|
|
14
|
-
const
|
|
23
|
+
const RECOVERIES = new Set<string>(BENCHMARK_DOMAIN_RECOVERIES)
|
|
24
|
+
const CONFIG_KEYS = ['argv_prefix', 'cwd', 'enabled', 'executable', 'isolation', 'max_output_bytes', 'schema_version', 'terminal_output', 'timeout_ms', 'tools']
|
|
25
|
+
const INPUT_SCHEMA_MAX_BYTES = 16 * 1024
|
|
26
|
+
const INPUT_SCHEMA_MAX_DEPTH = 8
|
|
27
|
+
const INPUT_SCHEMA_MAX_NODES = 256
|
|
28
|
+
const INPUT_SCHEMA_MAX_PROPERTIES = 256
|
|
29
|
+
const INPUT_SCHEMA_MAX_ENUM_VALUES = 256
|
|
30
|
+
const INPUT_SCHEMA_MAX_DESCRIPTION_LENGTH = 512
|
|
31
|
+
const INPUT_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null', 'array', 'object'])
|
|
15
32
|
|
|
16
33
|
export interface BenchmarkEnvironmentBridgeConfig {
|
|
17
34
|
schema_version: typeof SCHEMA_VERSION
|
|
@@ -19,8 +36,7 @@ export interface BenchmarkEnvironmentBridgeConfig {
|
|
|
19
36
|
executable: string
|
|
20
37
|
cwd: string
|
|
21
38
|
argv_prefix: string[]
|
|
22
|
-
|
|
23
|
-
allowed_output_keys?: Record<string, string[]>
|
|
39
|
+
tools: BenchmarkToolDescriptor[]
|
|
24
40
|
timeout_ms: number
|
|
25
41
|
max_output_bytes: number
|
|
26
42
|
terminal_output: TerminalOutputConfig
|
|
@@ -31,10 +47,56 @@ export interface BenchmarkEnvironmentBridgeConfig {
|
|
|
31
47
|
}
|
|
32
48
|
}
|
|
33
49
|
|
|
50
|
+
export interface BenchmarkToolDescriptor {
|
|
51
|
+
name: string
|
|
52
|
+
description: string
|
|
53
|
+
input_schema: ObjectJsonSchema
|
|
54
|
+
output_keys: string[]
|
|
55
|
+
domain_outcomes: Array<{ status: 'miss' | 'error'; code: string; recovery: BenchmarkDomainRecovery }>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function descriptorSchema(tools: readonly BenchmarkToolDescriptor[]): JsonSchemaNode {
|
|
59
|
+
return {
|
|
60
|
+
oneOf: [
|
|
61
|
+
{
|
|
62
|
+
type: 'object',
|
|
63
|
+
description: 'List the frozen benchmark tool descriptors.',
|
|
64
|
+
properties: { action: { type: 'string', const: 'tools' } },
|
|
65
|
+
required: ['action'],
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'object',
|
|
70
|
+
description: 'List the frozen bridge error contract.',
|
|
71
|
+
properties: { action: { type: 'string', const: 'errors' } },
|
|
72
|
+
required: ['action'],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
...tools.map<JsonSchemaNode>(tool => ({
|
|
76
|
+
type: 'object',
|
|
77
|
+
description: tool.description,
|
|
78
|
+
properties: {
|
|
79
|
+
action: { type: 'string', const: 'call' },
|
|
80
|
+
tool: { type: 'string', const: tool.name },
|
|
81
|
+
arguments: tool.input_schema,
|
|
82
|
+
},
|
|
83
|
+
required: ['action', 'tool', 'arguments'],
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
})),
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
34
90
|
function plainObject(value: unknown): value is Record<string, unknown> {
|
|
35
91
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
36
92
|
}
|
|
37
93
|
|
|
94
|
+
function deepFreezeJson<T>(value: T): T {
|
|
95
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value
|
|
96
|
+
for (const child of Object.values(value as Record<string, unknown>)) deepFreezeJson(child)
|
|
97
|
+
return Object.freeze(value)
|
|
98
|
+
}
|
|
99
|
+
|
|
38
100
|
function existingExecutable(value: unknown): value is string {
|
|
39
101
|
if (typeof value !== 'string' || !isAbsolute(value)) return false
|
|
40
102
|
try {
|
|
@@ -55,17 +117,111 @@ function existingDirectory(value: unknown): value is string {
|
|
|
55
117
|
}
|
|
56
118
|
}
|
|
57
119
|
|
|
58
|
-
function identifiers(value: unknown): value is string[] {
|
|
59
|
-
return Array.isArray(value) && value.length > 0 && value.length <= 64 && new Set(value).size === value.length && value.every(
|
|
120
|
+
function identifiers(value: unknown, allowEmpty = false): value is string[] {
|
|
121
|
+
return Array.isArray(value) && (allowEmpty || value.length > 0) && value.length <= 64 && new Set(value).size === value.length && value.every(
|
|
60
122
|
item => typeof item === 'string' && item.length > 0 && IDENTIFIER.test(item),
|
|
61
123
|
)
|
|
62
124
|
}
|
|
63
125
|
|
|
64
|
-
function
|
|
126
|
+
function exactKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
127
|
+
return Object.keys(value).every(key => allowed.includes(key))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validDescription(value: unknown, required = false): boolean {
|
|
131
|
+
if (value === undefined) return !required
|
|
132
|
+
return typeof value === 'string'
|
|
133
|
+
&& (!required || value.trim().length > 0)
|
|
134
|
+
&& value.length <= INPUT_SCHEMA_MAX_DESCRIPTION_LENGTH
|
|
135
|
+
&& !/[\u0000-\u001f\u007f]/.test(value)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scalarMatches(type: string, value: unknown): boolean {
|
|
139
|
+
if (type === 'string') return typeof value === 'string' && value.length <= 512
|
|
140
|
+
if (type === 'number') return typeof value === 'number' && Number.isFinite(value)
|
|
141
|
+
if (type === 'integer') return typeof value === 'number' && Number.isSafeInteger(value)
|
|
142
|
+
if (type === 'boolean') return typeof value === 'boolean'
|
|
143
|
+
return type === 'null' && value === null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validInputSchema(value: unknown): value is ObjectJsonSchema {
|
|
65
147
|
if (!plainObject(value)) return false
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
148
|
+
let serialized: string
|
|
149
|
+
try {
|
|
150
|
+
serialized = JSON.stringify(value)
|
|
151
|
+
} catch {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
if (Buffer.byteLength(serialized, 'utf8') > INPUT_SCHEMA_MAX_BYTES) return false
|
|
155
|
+
|
|
156
|
+
const pending: Array<{ node: Record<string, unknown>; depth: number }> = [{ node: value, depth: 0 }]
|
|
157
|
+
const seen = new Set<object>()
|
|
158
|
+
let nodes = 0
|
|
159
|
+
let properties = 0
|
|
160
|
+
let enumValues = 0
|
|
161
|
+
while (pending.length > 0) {
|
|
162
|
+
const { node, depth } = pending.pop()!
|
|
163
|
+
if (seen.has(node) || ++nodes > INPUT_SCHEMA_MAX_NODES || depth > INPUT_SCHEMA_MAX_DEPTH) return false
|
|
164
|
+
seen.add(node)
|
|
165
|
+
if (!INPUT_SCHEMA_TYPES.has(String(node.type)) || !validDescription(node.description)) return false
|
|
166
|
+
|
|
167
|
+
if (node.type === 'object') {
|
|
168
|
+
if (!exactKeys(node, ['type', 'description', 'properties', 'required', 'additionalProperties'])
|
|
169
|
+
|| !plainObject(node.properties)
|
|
170
|
+
|| !Array.isArray(node.required)
|
|
171
|
+
|| node.additionalProperties !== false) return false
|
|
172
|
+
const propertyEntries = Object.entries(node.properties)
|
|
173
|
+
properties += propertyEntries.length
|
|
174
|
+
if (properties > INPUT_SCHEMA_MAX_PROPERTIES || propertyEntries.some(([key, child]) => !IDENTIFIER.test(key) || !plainObject(child))) return false
|
|
175
|
+
const required = node.required
|
|
176
|
+
if (required.length > propertyEntries.length
|
|
177
|
+
|| new Set(required).size !== required.length
|
|
178
|
+
|| required.some(key => typeof key !== 'string' || !Object.hasOwn(node.properties as Record<string, unknown>, key))) return false
|
|
179
|
+
for (const [, child] of propertyEntries) pending.push({ node: child as Record<string, unknown>, depth: depth + 1 })
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (node.type === 'array') {
|
|
184
|
+
if (!exactKeys(node, ['type', 'description', 'items']) || !plainObject(node.items)) return false
|
|
185
|
+
pending.push({ node: node.items, depth: depth + 1 })
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!exactKeys(node, ['type', 'description', 'enum', 'const'])
|
|
190
|
+
|| (Object.hasOwn(node, 'enum') && Object.hasOwn(node, 'const'))) return false
|
|
191
|
+
if (Object.hasOwn(node, 'const') && !scalarMatches(String(node.type), node.const)) return false
|
|
192
|
+
if (Object.hasOwn(node, 'enum')) {
|
|
193
|
+
if (!Array.isArray(node.enum) || node.enum.length === 0 || node.enum.length > 64) return false
|
|
194
|
+
enumValues += node.enum.length
|
|
195
|
+
if (enumValues > INPUT_SCHEMA_MAX_ENUM_VALUES
|
|
196
|
+
|| new Set(node.enum).size !== node.enum.length
|
|
197
|
+
|| node.enum.some(item => !scalarMatches(String(node.type), item))) return false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return value.type === 'object'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function toolDescriptors(value: unknown): value is BenchmarkToolDescriptor[] {
|
|
204
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64) return false
|
|
205
|
+
const names = new Set<string>()
|
|
206
|
+
return value.every(item => {
|
|
207
|
+
if (!plainObject(item)) return false
|
|
208
|
+
const keys = Object.keys(item).sort()
|
|
209
|
+
const required = ['description', 'domain_outcomes', 'input_schema', 'name']
|
|
210
|
+
if (JSON.stringify(keys) !== JSON.stringify([...required, 'output_keys'].sort())) return false
|
|
211
|
+
if (typeof item.name !== 'string' || !IDENTIFIER.test(item.name) || names.has(item.name)) return false
|
|
212
|
+
names.add(item.name)
|
|
213
|
+
if (!validDescription(item.description, true) || !validInputSchema(item.input_schema)) return false
|
|
214
|
+
if (!Array.isArray(item.domain_outcomes) || item.domain_outcomes.length > 64) return false
|
|
215
|
+
const outcomeKeys = new Set<string>()
|
|
216
|
+
if (!item.domain_outcomes.every(outcome => plainObject(outcome)
|
|
217
|
+
&& JSON.stringify(Object.keys(outcome).sort()) === JSON.stringify(['code', 'recovery', 'status'])
|
|
218
|
+
&& (outcome.status === 'miss' || outcome.status === 'error')
|
|
219
|
+
&& typeof outcome.code === 'string' && IDENTIFIER.test(outcome.code)
|
|
220
|
+
&& typeof outcome.recovery === 'string' && RECOVERIES.has(outcome.recovery)
|
|
221
|
+
&& !outcomeKeys.has(outcome.code)
|
|
222
|
+
&& (outcomeKeys.add(outcome.code), true))) return false
|
|
223
|
+
return identifiers(item.output_keys)
|
|
224
|
+
})
|
|
69
225
|
}
|
|
70
226
|
|
|
71
227
|
// These are passed as direct argv entries (never through a shell), so options
|
|
@@ -98,20 +254,19 @@ function validBound(value: unknown, maximum: number): value is number {
|
|
|
98
254
|
function validate(value: unknown): value is BenchmarkEnvironmentBridgeConfig {
|
|
99
255
|
const configKeys = plainObject(value) ? Object.keys(value).sort() : []
|
|
100
256
|
const baseKeys = [...CONFIG_KEYS].sort()
|
|
101
|
-
const extendedKeys = [...CONFIG_KEYS, ...OPTIONAL_CONFIG_KEYS].sort()
|
|
102
257
|
if (!plainObject(value)
|
|
103
|
-
||
|
|
258
|
+
|| JSON.stringify(configKeys) !== JSON.stringify(baseKeys)
|
|
104
259
|
|| value.schema_version !== SCHEMA_VERSION
|
|
105
260
|
|| value.enabled !== true
|
|
106
261
|
|| !existingExecutable(value.executable)
|
|
107
262
|
|| !existingDirectory(value.cwd)
|
|
108
263
|
|| !argvPrefix(value.argv_prefix)
|
|
109
|
-
|| !
|
|
264
|
+
|| !toolDescriptors(value.tools)
|
|
110
265
|
|| !validBound(value.timeout_ms, 120_000)
|
|
111
266
|
|| !validBound(value.max_output_bytes, 10 * 1024 * 1024)
|
|
112
267
|
|| !validateTerminalOutputConfig(value.terminal_output)
|
|
113
268
|
|| !exactIsolation(value.isolation)
|
|
114
|
-
|
|
269
|
+
) return false
|
|
115
270
|
return true
|
|
116
271
|
}
|
|
117
272
|
|
|
@@ -124,13 +279,13 @@ export function loadBenchmarkEnvironmentConfig(path: string): BenchmarkEnvironme
|
|
|
124
279
|
if (typeof getuid !== 'function' || metadata.uid !== getuid()) return null
|
|
125
280
|
if ((metadata.mode & 0o022) !== 0) return null
|
|
126
281
|
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
127
|
-
return validate(parsed) ? parsed : null
|
|
282
|
+
return validate(parsed) ? deepFreezeJson(parsed) : null
|
|
128
283
|
} catch {
|
|
129
284
|
return null
|
|
130
285
|
}
|
|
131
286
|
}
|
|
132
287
|
|
|
133
|
-
type Register = (tool:
|
|
288
|
+
type Register = (tool: ToolDefinition) => void
|
|
134
289
|
interface CollectedText {
|
|
135
290
|
readFrom(offset: number): { text: string; nextOffset: number; lossy?: boolean }
|
|
136
291
|
}
|
|
@@ -176,6 +331,19 @@ const FORBIDDEN_OUTPUT_KEY_FRAGMENTS = [
|
|
|
176
331
|
'loadermetadata', 'oracle', 'reference', 'reward', 'score',
|
|
177
332
|
]
|
|
178
333
|
|
|
334
|
+
export const BRIDGE_ERROR_CONTRACT: Record<string, { recoverable: boolean; remedy: string }> = Object.freeze({
|
|
335
|
+
invalid_action: { recoverable: true, remedy: 'action 只允许 tools、call 或 errors' },
|
|
336
|
+
disallowed_tool: { recoverable: true, remedy: '先 action=tools 列出 allowed_tools 清单,再从清单内选工具' },
|
|
337
|
+
invalid_arguments: { recoverable: true, remedy: 'arguments 必须是可序列化 JSON 对象(≤64KB、深度≤12);收窄后重试' },
|
|
338
|
+
timed_out: { recoverable: true, remedy: '上游超时;可原样重试一次或换其他工具' },
|
|
339
|
+
output_truncated: { recoverable: true, remedy: '输出超出上限;让被调工具收窄查询范围后重试' },
|
|
340
|
+
invalid_json: { recoverable: true, remedy: '上游输出不是合法 JSON;重试一次或换工具' },
|
|
341
|
+
invalid_output: { recoverable: true, remedy: '上游输出结构不符;重试或换工具' },
|
|
342
|
+
runner_failed: { recoverable: true, remedy: '上游进程非零退出;可重试一次,持续失败换工具' },
|
|
343
|
+
spawn_failed: { recoverable: false, remedy: '可执行文件不可用——环境问题,调用方不可恢复' },
|
|
344
|
+
forbidden_output: { recoverable: false, remedy: '输出含未授权键(策略边界)——不可恢复,换工具或放弃' },
|
|
345
|
+
})
|
|
346
|
+
|
|
179
347
|
function inspectOutput(value: unknown): 'ok' | 'forbidden_key' | 'structure_limit' {
|
|
180
348
|
const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
|
|
181
349
|
let nodes = 0
|
|
@@ -199,19 +367,22 @@ function inspectOutput(value: unknown): 'ok' | 'forbidden_key' | 'structure_limi
|
|
|
199
367
|
|
|
200
368
|
function inspectAllowedOutput(value: unknown, allowedKeys: string[]): boolean {
|
|
201
369
|
const allowed = new Set(allowedKeys)
|
|
202
|
-
const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
|
|
370
|
+
const pending: Array<{ value: unknown; depth: number; coveredByDeclaredKey: boolean }> = [{ value, depth: 0, coveredByDeclaredKey: false }]
|
|
203
371
|
let nodes = 0
|
|
204
372
|
while (pending.length > 0) {
|
|
205
373
|
const current = pending.pop()!
|
|
206
374
|
if (++nodes > 10_000 || current.depth > 24) return false
|
|
207
375
|
if (Array.isArray(current.value)) {
|
|
208
|
-
for (const child of current.value) pending.push({ value: child, depth: current.depth + 1 })
|
|
376
|
+
for (const child of current.value) pending.push({ value: child, depth: current.depth + 1, coveredByDeclaredKey: current.coveredByDeclaredKey })
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
if (!plainObject(current.value)) {
|
|
380
|
+
if (!current.coveredByDeclaredKey) return false
|
|
209
381
|
continue
|
|
210
382
|
}
|
|
211
|
-
if (!plainObject(current.value)) continue
|
|
212
383
|
for (const [key, child] of Object.entries(current.value)) {
|
|
213
384
|
if (!allowed.has(key)) return false
|
|
214
|
-
pending.push({ value: child, depth: current.depth + 1 })
|
|
385
|
+
pending.push({ value: child, depth: current.depth + 1, coveredByDeclaredKey: true })
|
|
215
386
|
}
|
|
216
387
|
}
|
|
217
388
|
return true
|
|
@@ -254,31 +425,41 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
254
425
|
if (!bridge) throw new Error('benchmark environment bridge configuration unavailable')
|
|
255
426
|
if (!subprocess) throw new Error('benchmark environment bridge subprocess unavailable')
|
|
256
427
|
|
|
257
|
-
|
|
428
|
+
const parameters: Record<string, unknown> = deepFreezeJson({
|
|
429
|
+
oneOf: descriptorSchema(bridge.tools).oneOf,
|
|
430
|
+
})
|
|
431
|
+
assertSupportedJsonSchema(parameters)
|
|
432
|
+
|
|
433
|
+
const definition: ToolDefinition = {
|
|
258
434
|
name: 'gotry_benchmark_environment',
|
|
259
435
|
description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed.',
|
|
260
|
-
parameters
|
|
261
|
-
query: {
|
|
262
|
-
type: 'json',
|
|
263
|
-
required: true,
|
|
264
|
-
description: '{ action: "tools" | "call", tool?: string, arguments?: object }',
|
|
265
|
-
},
|
|
266
|
-
},
|
|
436
|
+
parameters,
|
|
267
437
|
output: {
|
|
268
|
-
schema: {
|
|
438
|
+
schema: {},
|
|
269
439
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
270
440
|
},
|
|
271
|
-
async execute(args:
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
441
|
+
async execute(args: unknown): Promise<Record<string, never>> {
|
|
442
|
+
const violations = validateJsonSchemaValue(parameters, args, 'arguments')
|
|
443
|
+
if (violations.length > 0) throw new ToolArgsError(violations)
|
|
444
|
+
const query = args as Record<string, unknown>
|
|
445
|
+
const tool = query.tool
|
|
446
|
+
const descriptor = bridge.tools.find(item => item.name === tool)
|
|
447
|
+
if (query.action === 'tools') return jsonObject({ ok: true, tools: bridge.tools })
|
|
448
|
+
if (query.action === 'errors') return jsonObject({ ok: true, errors: BRIDGE_ERROR_CONTRACT })
|
|
449
|
+
if (query.action !== 'call') {
|
|
450
|
+
return jsonObject({ ok: false, error: 'invalid_action' })
|
|
451
|
+
}
|
|
452
|
+
if (typeof tool !== 'string' || !descriptor) {
|
|
276
453
|
return jsonObject({ ok: false, error: 'disallowed_tool' })
|
|
277
454
|
}
|
|
278
|
-
if (
|
|
455
|
+
if (!plainObject(query.arguments)) {
|
|
456
|
+
return jsonObject({ ok: false, error: 'invalid_arguments' })
|
|
457
|
+
}
|
|
458
|
+
const callArguments = query.arguments
|
|
459
|
+
const argumentViolations = validateJsonSchemaValue(descriptor.input_schema, callArguments, 'arguments')
|
|
460
|
+
if (argumentViolations.length > 0) {
|
|
279
461
|
return jsonObject({ ok: false, error: 'invalid_arguments' })
|
|
280
462
|
}
|
|
281
|
-
const callArguments = query.arguments === undefined ? {} : query.arguments
|
|
282
463
|
const serialized = serializedArguments(callArguments)
|
|
283
464
|
if (serialized.reason) return jsonObject({ ok: false, error: 'invalid_arguments', reason: serialized.reason })
|
|
284
465
|
const controller = new AbortController()
|
|
@@ -296,7 +477,7 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
296
477
|
env.PYTHONDONTWRITEBYTECODE = '1'
|
|
297
478
|
env.PYTHONNOUSERSITE = '1'
|
|
298
479
|
handle = subprocess.spawn({
|
|
299
|
-
argv: [bridge.executable, ...bridge.argv_prefix, 'call',
|
|
480
|
+
argv: [bridge.executable, ...bridge.argv_prefix, 'call', tool, serialized.json!],
|
|
300
481
|
cwd: bridge.cwd,
|
|
301
482
|
stdio: {
|
|
302
483
|
stdin: 'ignore',
|
|
@@ -325,12 +506,18 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
325
506
|
const outputInspection = inspectOutput(parsed)
|
|
326
507
|
if (outputInspection === 'forbidden_key') return jsonObject({ ok: false, error: 'forbidden_output' })
|
|
327
508
|
if (outputInspection === 'structure_limit') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
328
|
-
|
|
509
|
+
if (parsed.schema_version !== BENCHMARK_TOOL_RESULT_SCHEMA_VERSION || typeof parsed.status !== 'string') {
|
|
510
|
+
return jsonObject({ ok: false, error: 'invalid_output' })
|
|
511
|
+
}
|
|
512
|
+
if (parsed.status === 'miss' || parsed.status === 'error') {
|
|
513
|
+
if (typeof parsed.code !== 'string' || typeof parsed.recovery !== 'string' || !descriptor.domain_outcomes.some(outcome => outcome.status === parsed.status && outcome.code === parsed.code && outcome.recovery === parsed.recovery)) return jsonObject({ ok: false, error: 'invalid_output' })
|
|
514
|
+
if (Object.keys(parsed).sort().join(',') !== 'code,recovery,schema_version,status') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
515
|
+
return jsonObject({ ok: true, outcome: parsed })
|
|
516
|
+
}
|
|
517
|
+
if (parsed.status !== 'ok' || !Object.hasOwn(parsed, 'result') || Object.keys(parsed).sort().join(',') !== 'result,schema_version,status') return jsonObject({ ok: false, error: 'invalid_output' })
|
|
518
|
+
const visibleResult = parsed.result
|
|
329
519
|
if (!plainObject(visibleResult) && !Array.isArray(visibleResult)) return jsonObject({ ok: false, error: 'invalid_output' })
|
|
330
|
-
|
|
331
|
-
? bridge.allowed_output_keys[query.tool]
|
|
332
|
-
: undefined
|
|
333
|
-
if (outputKeys && !inspectAllowedOutput(visibleResult, outputKeys)) return jsonObject({ ok: false, error: 'forbidden_output' })
|
|
520
|
+
if (!inspectAllowedOutput(visibleResult, descriptor.output_keys)) return jsonObject({ ok: false, error: 'forbidden_output' })
|
|
334
521
|
return jsonObject({ ok: true, result: visibleResult })
|
|
335
522
|
} catch {
|
|
336
523
|
if (controller.signal.aborted) return jsonObject({ ok: false, error: 'timed_out' })
|
|
@@ -339,10 +526,11 @@ export function registerBenchmarkEnvironmentBridge(
|
|
|
339
526
|
clearTimeout(timer)
|
|
340
527
|
}
|
|
341
528
|
},
|
|
342
|
-
}
|
|
529
|
+
}
|
|
530
|
+
register(definition)
|
|
343
531
|
return Object.freeze({
|
|
344
532
|
toolName: 'gotry_benchmark_environment',
|
|
345
|
-
allowedTools: Object.freeze(
|
|
533
|
+
allowedTools: Object.freeze(bridge.tools.map(item => item.name)),
|
|
346
534
|
terminal: Object.freeze({ ...bridge.terminal_output }),
|
|
347
535
|
})
|
|
348
536
|
}
|
package/ts/src/bookable-facts.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
package/ts/src/booking-saga.ts
CHANGED
|
@@ -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 字符串里)。
|