@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.17
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 +51 -13
- package/README.zh-CN.md +26 -12
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +357 -8
- package/bin/gotry-inner.js +442 -60
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +50 -0
- package/bin/gotry.js +1 -1
- package/cordis.gotry-patch.yml +15 -1
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/model-override.js +18 -0
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +358 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extension-distribution.js +234 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/golden-score.js +92 -0
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/static-flight-golden.js +137 -0
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +147 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +83 -1
- package/dist/capabilities/session-search.js +111 -2
- package/dist/capabilities/weather.js +168 -46
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +102 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-planning-budget-e2e.js +227 -0
- package/dist/scripts/agent-planning-budget-tests.js +173 -0
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
- package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
- package/dist/scripts/bootstrap-tests.js +59 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
- package/dist/scripts/dsh-runtime-closure.js +150 -0
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/evaluation-cadence-tests.js +347 -0
- package/dist/scripts/evaluation-contract-tests.js +574 -0
- package/dist/scripts/extension-distribution-cli.js +35 -0
- package/dist/scripts/extension-distribution-tests.js +341 -0
- package/dist/scripts/extension-tests.js +573 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/model-override-e2e.js +176 -0
- package/dist/scripts/nightly-evidence-tests.js +67 -4
- package/dist/scripts/nightly-evidence.js +59 -3
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +186 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +40 -4
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +70 -20
- package/dist/scripts/sf-live-benchmark.js +338 -0
- package/dist/scripts/sf-live-cli-tests.js +21 -0
- package/dist/scripts/sf-soft-score-tests.js +108 -0
- package/dist/scripts/sf-summary.js +93 -0
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke-session-gate-tests.js +29 -0
- package/dist/scripts/smoke.js +154 -33
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/static-golden-tests.js +299 -0
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +694 -44
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/benchmark-agent-conformance.js +370 -0
- package/dist/src/benchmark-environment-bridge.js +384 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
- package/dist/src/benchmark-tool-isolation.js +124 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/booking-surface/availability-policy-v2.js +830 -0
- package/dist/src/booking-surface/canonical-schema.js +113 -0
- package/dist/src/booking-surface/contracts-v2.js +89 -0
- package/dist/src/booking-surface/contracts.js +46 -0
- package/dist/src/booking-surface/dsh-planner.js +453 -0
- package/dist/src/booking-surface/dsh-plugin.js +93 -0
- package/dist/src/booking-surface/error-codes.js +94 -0
- package/dist/src/booking-surface/index.js +15 -0
- package/dist/src/booking-surface/profile.js +68 -0
- package/dist/src/booking-surface/runtime-v2.js +1771 -0
- package/dist/src/booking-surface/runtime.js +351 -0
- package/dist/src/booking-surface/server-v2.js +334 -0
- package/dist/src/booking-surface/server.js +302 -0
- package/dist/src/booking-surface/startup.js +159 -0
- package/dist/src/booking-surface/validation-v2.js +319 -0
- package/dist/src/booking-surface/validation.js +809 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/evaluation-cadence.js +234 -0
- package/dist/src/evaluation-contracts.js +906 -0
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +234 -60
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +2 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-budget.js +136 -0
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +50 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +291 -12
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +377 -0
- package/ts/capabilities/session/extension-channel.ts +95 -0
- package/ts/capabilities/session/extension-distribution.ts +264 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-login.ts +63 -2
- package/ts/capabilities/session-search.ts +105 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/benchmark-agent-conformance.ts +448 -0
- package/ts/src/benchmark-environment-bridge.ts +348 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +557 -0
- package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
- package/ts/src/booking-surface/canonical-schema.js +113 -0
- package/ts/src/booking-surface/contracts-v2.ts +118 -0
- package/ts/src/booking-surface/contracts.ts +380 -0
- package/ts/src/booking-surface/dsh-planner.ts +452 -0
- package/ts/src/booking-surface/dsh-plugin.js +93 -0
- package/ts/src/booking-surface/error-codes.ts +101 -0
- package/ts/src/booking-surface/index.ts +12 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/runtime-v2.ts +1466 -0
- package/ts/src/booking-surface/runtime.ts +483 -0
- package/ts/src/booking-surface/server-v2.ts +247 -0
- package/ts/src/booking-surface/server.ts +324 -0
- package/ts/src/booking-surface/startup.ts +196 -0
- package/ts/src/booking-surface/validation-v2.ts +205 -0
- package/ts/src/booking-surface/validation.ts +453 -0
- package/ts/src/index.ts +161 -14
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 产物事实闸 gotry_artifact_gate.v1(issue #46,ADR-19)。
|
|
3
|
+
*
|
|
4
|
+
* 最终 artifact 里的每个可下单事实必须能回溯到注册表内的 exact-date 工具结果
|
|
5
|
+
* (query_id 可重放);无法回溯 → 阻止以「已验证方案」输出。
|
|
6
|
+
*
|
|
7
|
+
* 工作方式:从 markdown 反向抽取 claim(航班号/承运直飞/通用直飞断言/机场映射/
|
|
8
|
+
* 政策/✓ 语气/联程措辞),逐条对照事实注册表与航司机场映射表;行程级口径
|
|
9
|
+
* (夜数/legs/预算下限)由结构化 itinerary 的不变量校验并入同一报告。
|
|
10
|
+
* 抽取是保守的:抽不出上下文的 claim 按 route_unqueried 处理(更重,不更轻)——
|
|
11
|
+
* fail closed,不放行「写得含糊所以查不到」的事实。
|
|
12
|
+
*
|
|
13
|
+
* 纯函数,零 I/O。映射表数据见 data/airline-airports.json(as_of 快照)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
flightClaimVerdict,
|
|
18
|
+
itineraryInvariants,
|
|
19
|
+
latestFactsForRouteDate,
|
|
20
|
+
type BookableFact,
|
|
21
|
+
type FlightClaim,
|
|
22
|
+
type FlightClaimVerdict,
|
|
23
|
+
type ItineraryFacts,
|
|
24
|
+
} from './bookable-facts.ts'
|
|
25
|
+
|
|
26
|
+
export const ARTIFACT_GATE_SCHEMA = 'gotry_artifact_gate.v1' as const
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// 映射表(data/airline-airports.json 的内存形状)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export interface AirlineAirportMap {
|
|
33
|
+
meta: { as_of: string; review_by?: string; source?: string; note?: string }
|
|
34
|
+
/** 中文城市名 → 主机场 IATA(航线上下文解析用,稳定词汇) */
|
|
35
|
+
city_alias: Record<string, string>
|
|
36
|
+
/** 双机场城市的航司→机场映射(冲突检测用,dated 快照):如 曼谷 { FD: DMK, VZ: BKK } */
|
|
37
|
+
carrier_airport: Record<string, Record<string, string>>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// claim 抽取
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export interface ExtractedFlightClaim extends FlightClaim {
|
|
45
|
+
line: number
|
|
46
|
+
text: string
|
|
47
|
+
/** 承运级直飞断言(无航班号,如「8L 直飞」/「东航直飞」) */
|
|
48
|
+
carrier_only?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ExtractedPolicyClaim {
|
|
52
|
+
line: number
|
|
53
|
+
text: string
|
|
54
|
+
has_as_of: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ExtractedAirportClaim {
|
|
58
|
+
line: number
|
|
59
|
+
text: string
|
|
60
|
+
carriers: string[]
|
|
61
|
+
airport: string
|
|
62
|
+
city: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ExtractedClaims {
|
|
66
|
+
flights: ExtractedFlightClaim[]
|
|
67
|
+
policies: ExtractedPolicyClaim[]
|
|
68
|
+
airports: ExtractedAirportClaim[]
|
|
69
|
+
/** 含「直飞」断言且可解析航线的行(通用直飞规则:该日无任何在架直飞即违例) */
|
|
70
|
+
direct_lines: Array<{ line: number; text: string; origin?: string; destination?: string; date?: string }>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 航班号:2 位承运码 + 3-4 位数字(UO784/EK328/MF1538/CZ8582/9C8781/FD597) */
|
|
74
|
+
const FLIGHT_NO = /(?<![A-Za-z0-9])([A-Z0-9]{2}\d{3,4})(?![\d])/g
|
|
75
|
+
/** 承运级直飞断言:「8L 直飞」「FZ 直飞香港」——无航班号的航线存在性断言 */
|
|
76
|
+
const CARRIER_DIRECT = /(?<![A-Za-z0-9])([A-Z0-9]{2})(?!\d)(?:\s|[一-龥]){0,6}直飞/g
|
|
77
|
+
/** 中文承运名 → 二字码(承运级断言覆盖;只列闸需要的常见出境承运) */
|
|
78
|
+
const CARRIER_ZH: Record<string, string> = {
|
|
79
|
+
东航: 'MU', 南航: 'CZ', 国航: 'CA', 厦航: 'MF', 春秋: '9C', 祥鹏: '8L',
|
|
80
|
+
亚航: 'FD', 越捷: 'VZ', 国泰: 'CX', 港航: 'HX', 香港快运: 'UO', 阿联酋: 'EK', 泰航: 'TG',
|
|
81
|
+
}
|
|
82
|
+
const POLICY_WORD = /免签|落地签|签证|入境申报|过境免/
|
|
83
|
+
/** as_of 必须是「截至 + 具体日期」——「现行 60 天」不算时间边界(issue #46 政策行) */
|
|
84
|
+
const AS_OF_WORD = /截至\s*\d{4}[-/年]\d{1,2}|as[_ ]?of\s*\d{4}/i
|
|
85
|
+
const CHECK_MARK = /[✓✅]/
|
|
86
|
+
|
|
87
|
+
/** 「直飞」断言(排除否定前置:无/不/没有/未见直飞;函数实现避开变宽 lookbehind 引擎差异) */
|
|
88
|
+
function hasDirectAssertion(line: string): boolean {
|
|
89
|
+
const idx = line.indexOf('直飞')
|
|
90
|
+
if (idx < 0) return false
|
|
91
|
+
const before = line.slice(Math.max(0, idx - 3), idx)
|
|
92
|
+
return !(before.endsWith('无') || before.endsWith('不') || before.endsWith('没有') || before.endsWith('未见'))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface SectionCtx { origin?: string; destination?: string; date?: string }
|
|
96
|
+
|
|
97
|
+
/** 从文本(小节标题或行)解析航线上下文:箭头紧邻城市对(方向最可靠)优先,否则按出现位置取前两个;日期 M.D 或 MM-DD */
|
|
98
|
+
function routeCtxOf(text: string, map: AirlineAirportMap, defaultYear?: number): SectionCtx {
|
|
99
|
+
const occ: Array<{ city: string; iata: string; pos: number }> = []
|
|
100
|
+
for (const [city, iata] of Object.entries(map.city_alias)) {
|
|
101
|
+
let pos = text.indexOf(city)
|
|
102
|
+
while (pos >= 0) {
|
|
103
|
+
occ.push({ city, iata, pos })
|
|
104
|
+
pos = text.indexOf(city, pos + city.length)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
occ.sort((a, b) => a.pos - b.pos)
|
|
108
|
+
const ctx: SectionCtx = {}
|
|
109
|
+
// 「迪拜10:05→深圳」「迪拜 → 深圳」——两城市间有箭头即取方向(表格单元格顺序不可靠)
|
|
110
|
+
outer: for (let i = 0; i < occ.length; i++) {
|
|
111
|
+
for (let j = i + 1; j < occ.length; j++) {
|
|
112
|
+
if (occ[j]!.iata === occ[i]!.iata) continue
|
|
113
|
+
const between = text.slice(occ[i]!.pos + occ[i]!.city.length, occ[j]!.pos)
|
|
114
|
+
if (/→|⇀|->/.test(between)) {
|
|
115
|
+
ctx.origin = occ[i]!.iata
|
|
116
|
+
ctx.destination = occ[j]!.iata
|
|
117
|
+
break outer
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!ctx.origin && occ.length >= 2) {
|
|
122
|
+
ctx.origin = occ[0]!.iata
|
|
123
|
+
ctx.destination = occ[1]!.iata
|
|
124
|
+
}
|
|
125
|
+
if (defaultYear) {
|
|
126
|
+
const dm = text.match(/(?<!\d)(\d{1,2})\.(\d{1,2})(?!\d)/) ?? text.match(/(?<!\d)(\d{2})-(\d{2})(?!\d)/)
|
|
127
|
+
if (dm) {
|
|
128
|
+
const mm = Number(dm[1]), dd = Number(dm[2])
|
|
129
|
+
if (mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31) {
|
|
130
|
+
ctx.date = `${defaultYear}-${String(mm).padStart(2, '0')}-${String(dd).padStart(2, '0')}`
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return ctx
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 从 markdown 抽取全部可下单 claim。
|
|
139
|
+
* 小节标题(### 段2|香港→普吉(7.17 周六))为下方行提供 route/date 上下文;
|
|
140
|
+
* 行内自带上下文优先。默认年份由 trip window 提供(远期行程的年份不含糊)。
|
|
141
|
+
*/
|
|
142
|
+
export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: { trip_year?: number }): ExtractedClaims {
|
|
143
|
+
const claims: ExtractedClaims = { flights: [], policies: [], airports: [], direct_lines: [] }
|
|
144
|
+
const lines = markdown.split('\n')
|
|
145
|
+
let section: SectionCtx = {}
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
const line = lines[i]!
|
|
148
|
+
const lineNo = i + 1
|
|
149
|
+
if (/^#{1,4}\s/.test(line)) {
|
|
150
|
+
const ctx = routeCtxOf(line, map, opts?.trip_year)
|
|
151
|
+
section = { origin: ctx.origin, destination: ctx.destination, date: ctx.date ?? section.date }
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
const own = routeCtxOf(line, map, opts?.trip_year)
|
|
155
|
+
const origin = own.origin ?? section.origin
|
|
156
|
+
const destination = own.destination ?? section.destination
|
|
157
|
+
const date = own.date ?? section.date
|
|
158
|
+
|
|
159
|
+
for (const m of line.matchAll(FLIGHT_NO)) {
|
|
160
|
+
claims.flights.push({ line: lineNo, text: line.trim().slice(0, 120), flight_no: m[1]!.toUpperCase(), origin, destination, date })
|
|
161
|
+
}
|
|
162
|
+
for (const m of line.matchAll(CARRIER_DIRECT)) {
|
|
163
|
+
const carrier = m[1]!.toUpperCase()
|
|
164
|
+
// 纯数字(时刻尾数,如「21:35 直飞」的 35)不是承运码;已是航班号前缀的不重复记
|
|
165
|
+
if (!/[A-Z]/.test(carrier)) continue
|
|
166
|
+
if (new RegExp(`${carrier}\\d`).test(line)) continue
|
|
167
|
+
claims.flights.push({ line: lineNo, text: line.trim().slice(0, 120), flight_no: '', carrier_only: carrier, origin, destination, date })
|
|
168
|
+
}
|
|
169
|
+
if (hasDirectAssertion(line) && (origin || destination)) {
|
|
170
|
+
claims.direct_lines.push({ line: lineNo, text: line.trim().slice(0, 120), origin, destination, date })
|
|
171
|
+
// 中文承运名直飞断言(「东航 南京→普吉(白天班)」无二字码,FLIGHT_NO/CARRIER_DIRECT 都漏);
|
|
172
|
+
// 词首不得紧贴汉字——「国泰航空」里的「泰航」不是泰航
|
|
173
|
+
for (const [zh, code] of Object.entries(CARRIER_ZH)) {
|
|
174
|
+
if (new RegExp(`(?<![一-龥])${zh}`).test(line) && !new RegExp(`${code}\\d`).test(line) && !claims.flights.some(c => c.line === lineNo && c.carrier_only === code)) {
|
|
175
|
+
claims.flights.push({ line: lineNo, text: line.trim().slice(0, 120), flight_no: '', carrier_only: code, origin, destination, date })
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (POLICY_WORD.test(line)) {
|
|
180
|
+
claims.policies.push({ line: lineNo, text: line.trim().slice(0, 160), has_as_of: AS_OF_WORD.test(line) })
|
|
181
|
+
}
|
|
182
|
+
// 航司→机场映射 claim:同一行出现承运码 + 机场码,且该城是双机场映射面
|
|
183
|
+
for (const [city, carriers] of Object.entries(map.carrier_airport)) {
|
|
184
|
+
const airports = [...new Set(Object.values(carriers))]
|
|
185
|
+
if (!airports.some(a => line.includes(a))) continue
|
|
186
|
+
const mentioned = Object.keys(carriers).filter(c => new RegExp(`(?<![A-Za-z0-9])${c}(?![A-Za-z0-9])`).test(line))
|
|
187
|
+
if (mentioned.length === 0) continue
|
|
188
|
+
const airport = airports.find(a => line.includes(a))
|
|
189
|
+
if (airport) claims.airports.push({ line: lineNo, text: line.trim().slice(0, 120), carriers: mentioned, airport, city })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return claims
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// 闸:逐条对照注册表与映射表;行程级口径由结构化 itinerary 不变量并入
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export type GateViolationKind =
|
|
200
|
+
| FlightClaimVerdict // not_in_source / route_unqueried / contradicted(traceable 不产生违例)
|
|
201
|
+
| 'carrier_direct_unverified' // 承运级直飞断言无 exact-date 证据(「可考虑 8L 直飞」)
|
|
202
|
+
| 'airport_mapping_conflict' // 航司→机场与 dated 映射冲突(FD/VZ 均落 DMK)
|
|
203
|
+
| 'policy_without_as_of' // 政策断言无「截至 YYYY-MM-DD」时间边界
|
|
204
|
+
| 'unconditional_check' // 对未核验/不可售 claim 使用无条件 ✓/✅
|
|
205
|
+
| 'self_transfer_called_through' // 分票/自助转机被称为「联程」
|
|
206
|
+
| 'nights_inconsistent' // 酒店夜+机上夜 ≠ 行程总夜数
|
|
207
|
+
| 'legs_inconsistent' // flight O&D 段与 legs 口径不一致
|
|
208
|
+
| 'budget_floor_inconsistent' // 预算下限话术低于分项最低合计
|
|
209
|
+
| 'date_order' // 住宿/段日期越窗或倒挂
|
|
210
|
+
|
|
211
|
+
export interface GateViolation {
|
|
212
|
+
kind: GateViolationKind
|
|
213
|
+
line: number
|
|
214
|
+
detail: string
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface GateReport {
|
|
218
|
+
schema: typeof ARTIFACT_GATE_SCHEMA
|
|
219
|
+
verdict: 'pass' | 'blocked'
|
|
220
|
+
claims_checked: number
|
|
221
|
+
traceable: number
|
|
222
|
+
violations: GateViolation[]
|
|
223
|
+
/** blocked 时的措辞纪律:不得宣称「已验证方案」 */
|
|
224
|
+
presentation: 'verified_itinerary_allowed' | 'verified_label_forbidden'
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 行内 HH:MM 与已溯源事实的时刻对照:写了时刻却一个都对不上 = 混入旧航季(contradicted) */
|
|
228
|
+
function lineTimesContradict(text: string, dep?: string, arr?: string): boolean {
|
|
229
|
+
if (!dep && !arr) return false
|
|
230
|
+
const times = [...text.matchAll(/(?<!\d)(\d{1,2}:\d{2})(?!\d)/g)].map(m => m[1]!)
|
|
231
|
+
if (times.length === 0) return false
|
|
232
|
+
return !times.some(t => t === dep || t === arr)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function gateArtifact(
|
|
236
|
+
markdown: string,
|
|
237
|
+
facts: BookableFact[],
|
|
238
|
+
map: AirlineAirportMap,
|
|
239
|
+
opts?: { trip_year?: number; itinerary?: ItineraryFacts },
|
|
240
|
+
): GateReport {
|
|
241
|
+
const claims = extractClaims(markdown, map, opts)
|
|
242
|
+
const violations: GateViolation[] = []
|
|
243
|
+
let traceable = 0
|
|
244
|
+
|
|
245
|
+
for (const c of claims.flights) {
|
|
246
|
+
if (c.carrier_only) {
|
|
247
|
+
// 承运级直飞断言:注册表该 route+date 须存在该承运的 bookable 直飞
|
|
248
|
+
const hasCarrier = facts.some(f =>
|
|
249
|
+
f.kind === 'flight' && f.bookability === 'bookable_exact_date' && f.nonstop !== false
|
|
250
|
+
&& f.flight_no.startsWith(c.carrier_only!)
|
|
251
|
+
&& (!c.origin || f.route.origin === c.origin || f.route.origin_airport === c.origin)
|
|
252
|
+
&& (!c.destination || f.route.destination === c.destination || f.route.dest_airport === c.destination)
|
|
253
|
+
&& (!c.date || f.date === c.date))
|
|
254
|
+
if (!hasCarrier) {
|
|
255
|
+
const scoped = c.origin && c.destination && c.date
|
|
256
|
+
? latestFactsForRouteDate(facts, c.origin, c.destination, c.date)
|
|
257
|
+
: []
|
|
258
|
+
violations.push({
|
|
259
|
+
kind: scoped.length > 0 ? 'not_in_source' : 'carrier_direct_unverified',
|
|
260
|
+
line: c.line,
|
|
261
|
+
detail: `承运级直飞断言「${c.carrier_only} 直飞」无 exact-date 证据(${c.origin ?? '?'}→${c.destination ?? '?'} ${c.date ?? '?'})`
|
|
262
|
+
+ (scoped.length > 0 && scoped.every(f => f.bookability === 'bookable_exact_date' && f.nonstop === false)
|
|
263
|
+
? '——该日可售仅有中转' : '——「可考虑/也许有」不得写入决策树'),
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
const r = flightClaimVerdict(facts, c)
|
|
269
|
+
if (r.verdict === 'traceable') {
|
|
270
|
+
// 时刻一致性:溯源命中但行内时刻与事实全不符 = 旧航季混入(issue #46 EK328 行)
|
|
271
|
+
if (lineTimesContradict(c.text, r.fact?.dep_local, r.fact?.arr_local)) {
|
|
272
|
+
violations.push({
|
|
273
|
+
kind: 'contradicted',
|
|
274
|
+
line: c.line,
|
|
275
|
+
detail: `${c.flight_no} 时刻与 exact-date 快照不符(快照 ${r.fact?.dep_local}→${r.fact?.arr_local} ${r.fact?.date})——渲染层不得改写工具返回的时刻`,
|
|
276
|
+
})
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
traceable++
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
violations.push({ kind: r.verdict, line: c.line, detail: `${c.flight_no}: ${r.reason}` })
|
|
283
|
+
// 对不可述 claim 的无条件 ✓/✅ 追加语气违例(验收⑦:远期班期/价格不用无条件 ✓)
|
|
284
|
+
if (CHECK_MARK.test(c.text)) {
|
|
285
|
+
violations.push({ kind: 'unconditional_check', line: c.line, detail: `对未核验航班 ${c.flight_no} 使用无条件 ✓/✅——只有 bookable_exact_date 才允许确定性标记` })
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 通用直飞规则:该行未点名承运时,要求该 route+date 存在任一在架直飞;
|
|
290
|
+
// 「该日可售仅有中转/0 条」却写直飞 = 无证据分支(issue #46「DMK→KMG 可考虑 8L 直飞」行同款)
|
|
291
|
+
for (const d of claims.direct_lines) {
|
|
292
|
+
if (claims.flights.some(c => c.line === d.line)) continue // 航班号/承运级断言已逐条判过
|
|
293
|
+
if (!d.origin || !d.destination || !d.date) {
|
|
294
|
+
violations.push({ kind: 'route_unqueried', line: d.line, detail: `直飞断言缺 route/date 上下文,无法回溯——fail closed 按未核验处理` })
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
const scoped = latestFactsForRouteDate(facts, d.origin, d.destination, d.date)
|
|
298
|
+
if (scoped.length === 0) {
|
|
299
|
+
violations.push({ kind: 'route_unqueried', line: d.line, detail: `${d.origin}→${d.destination} ${d.date} 的直飞断言从未经 exact-date 源核验` })
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
const nonstopCount = scoped.filter(f => f.bookability === 'bookable_exact_date' && f.nonstop !== false).length
|
|
303
|
+
if (nonstopCount === 0) {
|
|
304
|
+
violations.push({ kind: 'not_in_source', line: d.line, detail: `${d.origin}→${d.destination} ${d.date} exact-date 无任何在架直飞(仅中转或 0 条)——不得写直飞` })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const a of claims.airports) {
|
|
309
|
+
const table = map.carrier_airport[a.city] ?? {}
|
|
310
|
+
for (const carrier of a.carriers) {
|
|
311
|
+
const expected = table[carrier]
|
|
312
|
+
if (expected && expected !== a.airport) {
|
|
313
|
+
violations.push({
|
|
314
|
+
kind: 'airport_mapping_conflict',
|
|
315
|
+
line: a.line,
|
|
316
|
+
detail: `${carrier} 在${a.city}落 ${expected}(映射快照 as_of ${map.meta.as_of}),产物写 ${a.airport}——双机场不互换,航司不合并映射`,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const p of claims.policies) {
|
|
323
|
+
if (!p.has_as_of) {
|
|
324
|
+
violations.push({ kind: 'policy_without_as_of', line: p.line, detail: `政策断言缺时间边界:动态政策只能表述为「截至 YYYY-MM-DD 的现行政策」并附复核日期` })
|
|
325
|
+
}
|
|
326
|
+
if (CHECK_MARK.test(p.text) && !p.has_as_of) {
|
|
327
|
+
violations.push({ kind: 'unconditional_check', line: p.line, detail: `政策行使用无条件 ✓/✅` })
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 联程措辞:含「联程」的行,逐航班号查 protected_connection——
|
|
332
|
+
// 只有全部航腿同票保护才允许称联程;分票/自助转机必须显式标红
|
|
333
|
+
markdown.split('\n').forEach((line, i) => {
|
|
334
|
+
if (!line.includes('联程')) return
|
|
335
|
+
const nos = [...line.matchAll(FLIGHT_NO)].map(m => m[1]!.toUpperCase())
|
|
336
|
+
const matched = facts.filter((f): f is Extract<BookableFact, { kind: 'flight' | 'train' }> =>
|
|
337
|
+
f.kind !== 'policy' && nos.includes(f.flight_no.toUpperCase()))
|
|
338
|
+
const allProtected = nos.length > 0
|
|
339
|
+
&& matched.length === nos.length
|
|
340
|
+
&& matched.every(f => f.protected_connection === true)
|
|
341
|
+
if (!allProtected) {
|
|
342
|
+
violations.push({
|
|
343
|
+
kind: 'self_transfer_called_through',
|
|
344
|
+
line: i + 1,
|
|
345
|
+
detail: '「联程」措辞无 protected_connection=true 证据——两段接得上不等于同票联程;分票必须标红自助转机(无行李直挂/误机无保护)',
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// 行程级口径不变量(夜数/legs/预算下限):结构化 itinerary 机器断言并入同一报告
|
|
351
|
+
if (opts?.itinerary) {
|
|
352
|
+
for (const v of itineraryInvariants(opts.itinerary)) {
|
|
353
|
+
violations.push({ kind: v.kind, line: 0, detail: v.detail })
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
schema: ARTIFACT_GATE_SCHEMA,
|
|
359
|
+
verdict: violations.length > 0 ? 'blocked' : 'pass',
|
|
360
|
+
claims_checked: claims.flights.length + claims.policies.length + claims.airports.length,
|
|
361
|
+
traceable,
|
|
362
|
+
violations,
|
|
363
|
+
presentation: violations.length > 0 ? 'verified_label_forbidden' : 'verified_itinerary_allowed',
|
|
364
|
+
}
|
|
365
|
+
}
|