@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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 +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
package/ts/src/artifact-gate.ts
CHANGED
|
@@ -18,10 +18,16 @@ import {
|
|
|
18
18
|
hotelClaimVerdict,
|
|
19
19
|
itineraryInvariants,
|
|
20
20
|
latestFactsForRouteDate,
|
|
21
|
+
railClaimVerdict,
|
|
22
|
+
renderFlightFact,
|
|
23
|
+
renderHotelFact,
|
|
24
|
+
renderPolicyFact,
|
|
21
25
|
type BookableFact,
|
|
22
26
|
type FlightClaim,
|
|
23
27
|
type FlightClaimVerdict,
|
|
24
28
|
type ItineraryFacts,
|
|
29
|
+
type PolicyFact,
|
|
30
|
+
type RailClaim,
|
|
25
31
|
} from './bookable-facts.ts'
|
|
26
32
|
|
|
27
33
|
export const ARTIFACT_GATE_SCHEMA = 'gotry_artifact_gate.v1' as const
|
|
@@ -49,6 +55,19 @@ export interface ExtractedFlightClaim extends FlightClaim {
|
|
|
49
55
|
carrier_only?: string
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
/** 车次 claim(issue #299):中文高铁/动车/城际/直达代码(G/D/C/Z + 3–4 位)。
|
|
59
|
+
* 闸侧不与航班 claim 合并;`railClaimVerdict` 只在 train 事实里查同号回溯,
|
|
60
|
+
* 缺事实/缺上下文 fail-closed(`rail_claim_unverified`),不得用历史班期或
|
|
61
|
+
* static-schedule 凑合格回溯。`gotry_flyai_search kind:'train'` 经
|
|
62
|
+
* `factsFromFlyai` 与 12306 `sessionTrainSearch` 均沿现有 typed fact log 接线;
|
|
63
|
+
* session 事实须通过 typed parser outcome + seat availability/freshness contract,
|
|
64
|
+
* 列表不含票价;
|
|
65
|
+
* 不新增 RailFact 渲染器,真实供应商可售性仍需独立证据。 */
|
|
66
|
+
export interface ExtractedRailClaim extends RailClaim {
|
|
67
|
+
line: number
|
|
68
|
+
text: string
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
export interface ExtractedPolicyClaim {
|
|
53
72
|
line: number
|
|
54
73
|
text: string
|
|
@@ -74,6 +93,7 @@ export interface ExtractedHotelClaim {
|
|
|
74
93
|
|
|
75
94
|
export interface ExtractedClaims {
|
|
76
95
|
flights: ExtractedFlightClaim[]
|
|
96
|
+
trains: ExtractedRailClaim[]
|
|
77
97
|
policies: ExtractedPolicyClaim[]
|
|
78
98
|
airports: ExtractedAirportClaim[]
|
|
79
99
|
/** 含「直飞」断言且可解析航线的行(通用直飞规则:该日无任何在架直飞即违例) */
|
|
@@ -83,8 +103,14 @@ export interface ExtractedClaims {
|
|
|
83
103
|
anchors: Map<number, string>
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
/** 航班号:2 位承运码 + 3-4 位数字(UO784/EK328/MF1538/CZ8582/9C8781/FD597)
|
|
106
|
+
/** 航班号:2 位承运码 + 3-4 位数字(UO784/EK328/MF1538/CZ8582/9C8781/FD597)。
|
|
107
|
+
* 中文高铁/动车/城际/直达代码(G/D/C/Z + 3–4 位)由 `TRAIN_NO` 单独收集,
|
|
108
|
+
* 不进入航班 claim,issue #299。 */
|
|
87
109
|
const FLIGHT_NO = /(?<![A-Za-z0-9])([A-Z0-9]{2}\d{3,4})(?![\d])/g
|
|
110
|
+
/** 车次号:中文 G/D/C/Z + 3-4 位数字(G1234/D3112/C2001/Z1/Z9999)。issue #299:
|
|
111
|
+
* 闸侧 train claim 集合与航班 claim 完全分离,`flightClaimVerdict` 不再吞 train
|
|
112
|
+
* 事实,`railClaimVerdict` 只在 train 事实里查同号回溯。 */
|
|
113
|
+
const TRAIN_NO = /(?<![A-Za-z0-9])([GDCZ]\d{3,4})(?![A-Za-z0-9])/g
|
|
88
114
|
/** 承运级直飞断言:「8L 直飞」「FZ 直飞香港」——无航班号的航线存在性断言 */
|
|
89
115
|
const CARRIER_DIRECT = /(?<![A-Za-z0-9])([A-Z0-9]{2})(?!\d)(?:\s|[一-龥]){0,6}直飞/g
|
|
90
116
|
/** 中文承运名 → 二字码(承运级断言覆盖;只列闸需要的常见出境承运) */
|
|
@@ -92,11 +118,34 @@ const CARRIER_ZH: Record<string, string> = {
|
|
|
92
118
|
东航: 'MU', 南航: 'CZ', 国航: 'CA', 厦航: 'MF', 春秋: '9C', 祥鹏: '8L',
|
|
93
119
|
亚航: 'FD', 越捷: 'VZ', 国泰: 'CX', 港航: 'HX', 香港快运: 'UO', 阿联酋: 'EK', 泰航: 'TG',
|
|
94
120
|
}
|
|
95
|
-
|
|
121
|
+
/** Finite hotel-category vocabulary shared by heading activation and row recognition (issue #301). */
|
|
122
|
+
const LODGING_VOCAB_RE = /酒店|住宿|客栈|民宿|度假村|青旅|青年旅舍|别墅|公寓/
|
|
123
|
+
/** Markdown ATX heading level 1–6 (根实测 #347:5/6 级 lodging heading 必须激活) */
|
|
124
|
+
const HEADING_ATX = /^(#{1,6})\s+(.+)$/
|
|
125
|
+
/** 「政策」关键词(issue #273 父 + 子 #302):覆盖签证/免签/落地签/海关/过境五大类 + 子 #302 显式列举的政策词有限并集;
|
|
126
|
+
* 海关申报 与 入境申报 同性质但被原 regex 漏掉,补一个 demonstrative miss。
|
|
127
|
+
* 子 #302 扩词边界:仅 EVUS/ETA/eVisa/疫苗/疫苗接种/健康申报/隔离/工作签/居留/返程签/护照有效期/黄皮书/保险;拉丁 token
|
|
128
|
+
* 大小写不敏感但不嵌入更长 Latin 词(REVUS ≠ EVUS);不做 NLP/同义词/任意政策词表扩展。
|
|
129
|
+
* 子 #381(D-26 R1)扩词:入境章/落地章/出境章/入境卡/出境卡/出入境卡——与落地签同族的
|
|
130
|
+
* 移民章/卡六词,根反例「普吉岛入境章费用 800元」原全零抽取静默过闸,缺「截至」按既有
|
|
131
|
+
* policy_without_as_of 显式拒绝。仍为有限机械词表,可列举扩充、不做语义模型。 */
|
|
132
|
+
const POLICY_WORD = /免签|落地签|入境章|落地章|出境章|入境卡|出境卡|出入境卡|签证|入境申报|海关申报|过境免|疫苗|疫苗接种|健康申报|隔离|工作签|居留|返程签|护照有效期|黄皮书|保险|(?<![A-Za-z])(?:EVUS|ETA|eVisa)(?![A-Za-z])/i
|
|
96
133
|
/** as_of 必须是「截至 + 具体日期」——「现行 60 天」不算时间边界(issue #46 政策行) */
|
|
97
134
|
const AS_OF_WORD = /截至\s*\d{4}[-/年]\d{1,2}|as[_ ]?of\s*\d{4}/i
|
|
98
135
|
const CHECK_MARK = /[✓✅]/
|
|
99
136
|
|
|
137
|
+
/** 交易承诺短语有限并集(D-26 R1,issue #381):费用/收费/押金/支付/缴费类下单措辞。
|
|
138
|
+
* 与「词表外金额写法」同一行同现、且该行经全部既有抽取原语后零 claim 时,按既有
|
|
139
|
+
* unverified_price_claim fail-closed。可列举扩充,不做 NLP/同义词/语义模型(#302 同款边界);
|
|
140
|
+
* 可订性短语(有房/可订)不在其中——由 §10/§12 酒店闸负责,其「裸短语不入闸」边界
|
|
141
|
+
* (#301/#347)保持不变。 */
|
|
142
|
+
const COMMITMENT_TRANSACTION_RE = /费用|收费|手续费|服务费|签证费|通行费|通关费|小费|押金|定金|订金|机场税|港口税|支付|付款|缴款|缴费|现付|到付|付现|代订|代购/
|
|
143
|
+
/** 受支持币种词表外的金额写法(D-26 R1,issue #381):`hardPricesInLine` 只识别
|
|
144
|
+
* ¥/[A-Z]{3} 前缀硬价;CJK 币种后缀({N}元/泰铢/美元…)与 $/$ 前缀是其覆盖不了的
|
|
145
|
+
* 下单金额写法(根反例「800元」)。¥/ISO 前缀金额不在其中——零 claim 行的
|
|
146
|
+
* ¥/ISO 金额(预算/约价口径)属 R2 价格容忍度 founder 决策边界,不由本网拒绝。 */
|
|
147
|
+
const OFF_VOCAB_AMOUNT_RE = /(?:\d[\d,]*(?:\.\d+)?\s*(?:元|美元|美金|泰铢|铢|日元|日圆|港币|港元|欧元|卢比|比索|林吉特|印尼盾|迪拉姆|里亚尔))|(?:[$$]\s*\d[\d,]*(?:\.\d+)?)/
|
|
148
|
+
|
|
100
149
|
/** 「直飞」断言(排除否定前置:无/不/没有/未见直飞;函数实现避开变宽 lookbehind 引擎差异) */
|
|
101
150
|
function hasDirectAssertion(line: string): boolean {
|
|
102
151
|
const idx = line.indexOf('直飞')
|
|
@@ -105,7 +154,7 @@ function hasDirectAssertion(line: string): boolean {
|
|
|
105
154
|
return !(before.endsWith('无') || before.endsWith('不') || before.endsWith('没有') || before.endsWith('未见'))
|
|
106
155
|
}
|
|
107
156
|
|
|
108
|
-
interface SectionCtx { origin?: string; destination?: string; date?: string }
|
|
157
|
+
interface SectionCtx { origin?: string; destination?: string; date?: string; /** 当前行是否在 lodging heading 上下文中(issue #301 无 token 行入闸) */ lodging?: boolean; /** 活动 lodging heading 的 ATX 层级栈(浅→深);同/更高级 non-lodging heading 弹出栈顶 */ lodgingDepths?: number[] }
|
|
109
158
|
|
|
110
159
|
/** 行内第一个命中词表的城市中文名(酒店 claim 的目的地键;无命中=undefined) */
|
|
111
160
|
function occ1(line: string, map: AirlineAirportMap): string | undefined {
|
|
@@ -161,7 +210,7 @@ function routeCtxOf(text: string, map: AirlineAirportMap, defaultYear?: number):
|
|
|
161
210
|
* 行内自带上下文优先。默认年份由 trip window 提供(远期行程的年份不含糊)。
|
|
162
211
|
*/
|
|
163
212
|
export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: { trip_year?: number }): ExtractedClaims {
|
|
164
|
-
const claims: ExtractedClaims = { flights: [], policies: [], airports: [], direct_lines: [], hotels: [], anchors: new Map() }
|
|
213
|
+
const claims: ExtractedClaims = { flights: [], trains: [], policies: [], airports: [], direct_lines: [], hotels: [], anchors: new Map() }
|
|
165
214
|
const lines = markdown.split('\n')
|
|
166
215
|
let section: SectionCtx = {}
|
|
167
216
|
// 渲染原语锚点(单向生成):带 fact:<id> 的行只走锚点确定性回溯,启发式抽取让位
|
|
@@ -174,9 +223,27 @@ export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: {
|
|
|
174
223
|
claims.anchors.set(lineNo, anchorMatch[1]!.toLowerCase())
|
|
175
224
|
continue
|
|
176
225
|
}
|
|
177
|
-
if (
|
|
226
|
+
if (HEADING_ATX.test(line)) {
|
|
178
227
|
const ctx = routeCtxOf(line, map, opts?.trip_year)
|
|
179
|
-
|
|
228
|
+
// lodging heading 上下文栈(issue #301 真实反例:无 token 行入闸):
|
|
229
|
+
// - lodging heading:在该层级压栈(更深栈顶替代外层语义,栈整体保留)
|
|
230
|
+
// - 同级/更高级 non-lodging heading:弹栈直到空
|
|
231
|
+
// - 更深的 non-lodging heading:继承栈
|
|
232
|
+
const headingMatch = line.match(HEADING_ATX)!
|
|
233
|
+
const headingLevel = headingMatch[1]!.length
|
|
234
|
+
const headingText = headingMatch[2]!
|
|
235
|
+
const isLodgingHeading = LODGING_VOCAB_RE.test(headingText)
|
|
236
|
+
const depths = (section.lodgingDepths ?? []).slice()
|
|
237
|
+
if (isLodgingHeading) {
|
|
238
|
+
// 同级 lodging heading 替换该层栈,更深的栈项保留
|
|
239
|
+
while (depths.length > 0 && depths[depths.length - 1]! >= headingLevel) depths.pop()
|
|
240
|
+
depths.push(headingLevel)
|
|
241
|
+
} else {
|
|
242
|
+
// 同级/更高级 non-lodging heading 弹出栈顶直至更浅或空
|
|
243
|
+
while (depths.length > 0 && depths[depths.length - 1]! >= headingLevel) depths.pop()
|
|
244
|
+
}
|
|
245
|
+
const lodging = depths.length > 0
|
|
246
|
+
section = { origin: ctx.origin, destination: ctx.destination, date: ctx.date ?? section.date, lodging, lodgingDepths: depths }
|
|
180
247
|
continue
|
|
181
248
|
}
|
|
182
249
|
const own = routeCtxOf(line, map, opts?.trip_year)
|
|
@@ -184,8 +251,18 @@ export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: {
|
|
|
184
251
|
const destination = own.destination ?? section.destination
|
|
185
252
|
const date = own.date ?? section.date
|
|
186
253
|
|
|
254
|
+
const trainNos = new Set([...line.matchAll(TRAIN_NO)].map(m => m[1]!.toUpperCase()))
|
|
255
|
+
for (const trainNo of trainNos) {
|
|
256
|
+
claims.trains.push({ line: lineNo, text: line.trim().slice(0, 120), flight_no: trainNo, origin, destination, date })
|
|
257
|
+
}
|
|
187
258
|
for (const m of line.matchAll(FLIGHT_NO)) {
|
|
188
|
-
|
|
259
|
+
const code = m[1]!.toUpperCase()
|
|
260
|
+
// 闸侧 train/flight 分离:中文高铁/动车/城际/直达前缀不得进入航班 claim。
|
|
261
|
+
// FLIGHT_NO 也会把 G1234 这类单字母车次整体匹配出来;只跳过同一行
|
|
262
|
+
// TRAIN_NO 实际命中的完整 token。不能按首字母跳过,否则合法双字母航司
|
|
263
|
+
// CZ8582 会被误删。
|
|
264
|
+
if (trainNos.has(code)) continue
|
|
265
|
+
claims.flights.push({ line: lineNo, text: line.trim().slice(0, 120), flight_no: code, origin, destination, date })
|
|
189
266
|
}
|
|
190
267
|
for (const m of line.matchAll(CARRIER_DIRECT)) {
|
|
191
268
|
const carrier = m[1]!.toUpperCase()
|
|
@@ -207,10 +284,26 @@ export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: {
|
|
|
207
284
|
if (POLICY_WORD.test(line)) {
|
|
208
285
|
claims.policies.push({ line: lineNo, text: line.trim().slice(0, 160), has_as_of: AS_OF_WORD.test(line) })
|
|
209
286
|
}
|
|
210
|
-
// 酒店 claim(D-26):断言可住性(有房/可订/已核验/✓)的行——目的地与档期缺失时 fail-closed
|
|
211
|
-
|
|
287
|
+
// 酒店 claim(D-26 + 子 #301 扩词):断言可住性(有房/可订/已核验/✓)的行——目的地与档期缺失时 fail-closed
|
|
288
|
+
// 父 #273 已有 酒店|住宿|客栈|民宿;子 #301 扩词覆盖 度假村/青旅/青年旅舍/别墅/公寓 五类非关键词住宿
|
|
289
|
+
// category(精品酒店 由 酒店 承接,不重复声明);lodging heading(§住宿/§酒店/§青旅 等,ATX 1–6)下完全无住宿类
|
|
290
|
+
// token 的可住断言也由 section.lodging 升为 hotel claim。行内匹配分两路:
|
|
291
|
+
// (a) 行内含 LODGING_VOCAB_RE 词表 token → 需 HOTEL_BOOKABLE 短语/标记
|
|
292
|
+
// (b) 行内无 token 但 lodging context 激活 → 需 HOTEL_BOOKABILITY 短语(有房/可订/.../可住),
|
|
293
|
+
// 单独的 ✓/✅ 不构成 bookability(例:`靠近地铁 ✓` 在 §住宿 下不是可订断言)。
|
|
294
|
+
// 检测到可订短语且带 ✓,未核验时再加 unconditional_check。
|
|
295
|
+
// 同级/更高级 non-lodging heading 退出上下文,更深 heading 继承;lodging depth 栈保留外层活动层级。
|
|
296
|
+
// 行内/heading 词汇同源 = LODGING_VOCAB_RE,不可漂移。无住宿类关键词仍走兜底 →
|
|
297
|
+
// unverifiable_hotel_claim(§10f 既有证据)。route/date section context 对航班逻辑不动;
|
|
298
|
+
// anchored 行由 `if (claims.anchors.has(c.line)) continue` 提前让位。
|
|
299
|
+
const HOTEL_BOOKABILITY = /有房|可订|在架|已核验|已验证|可住/
|
|
212
300
|
const HOTEL_BOOKABLE = /有房|可订|在架|已核验|已验证|可住|✓|✅/
|
|
213
|
-
|
|
301
|
+
const hasCategoryToken = LODGING_VOCAB_RE.test(line)
|
|
302
|
+
const inLodgingSection = !!section.lodging
|
|
303
|
+
const isBookabilityClaim = hasCategoryToken
|
|
304
|
+
? HOTEL_BOOKABLE.test(line)
|
|
305
|
+
: inLodgingSection && HOTEL_BOOKABILITY.test(line)
|
|
306
|
+
if (isBookabilityClaim) {
|
|
214
307
|
const stays = [...line.matchAll(/(?<!\d)(\d{4}-\d{2}-\d{2})(?!\d)/g)].map(m => m[1]!)
|
|
215
308
|
claims.hotels.push({
|
|
216
309
|
line: lineNo,
|
|
@@ -249,7 +342,10 @@ export type GateViolationKind =
|
|
|
249
342
|
| 'budget_floor_inconsistent' // 预算下限话术低于分项最低合计
|
|
250
343
|
| 'date_order' // 住宿/段日期越窗或倒挂
|
|
251
344
|
| 'unverifiable_hotel_claim' // 酒店可住断言无 exact-date 事实回溯(D-26)
|
|
345
|
+
| 'rail_claim_unverified' // 车次 claim 无 train exact-date 事实回溯(issue #299)
|
|
252
346
|
| 'fact_anchor_unknown' // 渲染锚点 fact:<id> 在注册表不存在(锚点被手改/伪造)
|
|
347
|
+
| 'unverified_price_claim' // 硬价缺少可比较的权威来源或可靠绑定(issue #300)
|
|
348
|
+
| 'price_contradicted' // 行内可靠硬价格与 exact-date 事实价格冲突(issue #300)
|
|
253
349
|
|
|
254
350
|
export interface GateViolation {
|
|
255
351
|
kind: GateViolationKind
|
|
@@ -275,11 +371,228 @@ function lineTimesContradict(text: string, dep?: string, arr?: string): boolean
|
|
|
275
371
|
return !times.some(t => t === dep || t === arr)
|
|
276
372
|
}
|
|
277
373
|
|
|
374
|
+
/** Renderer 固定的 reminder 短语正则(只此一句,日期为合法 ISO 阳历日)。
|
|
375
|
+
* 与 `policyCanonicalBody` 同源;闸侧只识别这一固定短语+位置(尾随在
|
|
376
|
+
* `[<source>@<fetched_at> #<query_id>]` 锚点块之前、收尾 `<!-- fact:<id> -->` 之前)。
|
|
377
|
+
* 用于 no-context 兼容性回退:从此正则抽日期,重建 `{...f, review_by: date}` 经
|
|
378
|
+
* renderer 还原整行,byte-for-byte 比对——不再手动拆 body/provenance。 */
|
|
379
|
+
const POLICY_REMINDER_PHRASE_RE = /;远期政策须复核——到 (\d{4}-\d{2}-\d{2}) 再核验一次/
|
|
380
|
+
|
|
381
|
+
interface HardPrice {
|
|
382
|
+
amount: number
|
|
383
|
+
currency: string
|
|
384
|
+
token: string
|
|
385
|
+
start: number
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 只抽取行内可可靠绑定的硬价。起价/约价/模糊值不进入比较,不支持的币种
|
|
390
|
+
* 只进入未核验分支;不做汇率换算,也不把酒店 priceRaw 带入这里(issue #300)。
|
|
391
|
+
*/
|
|
392
|
+
function hardPricesInLine(text: string): HardPrice[] {
|
|
393
|
+
const prices: HardPrice[] = []
|
|
394
|
+
const amount = String.raw`(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?`
|
|
395
|
+
const pattern = new RegExp(String.raw`(?:¥\s*(${amount})(?![\dA-Za-z,])|\b([A-Z]{3})\s*(${amount})(?![\dA-Za-z,]))`, 'gi')
|
|
396
|
+
for (const match of text.matchAll(pattern)) {
|
|
397
|
+
const amountText = match[1] ?? match[3]
|
|
398
|
+
if (!amountText || match.index === undefined) continue
|
|
399
|
+
const before = text.slice(Math.max(0, match.index - 8), match.index)
|
|
400
|
+
const after = text.slice(match.index + match[0].length, match.index + match[0].length + 8)
|
|
401
|
+
if (/(?:约|大约|约为|起价|起步|from)\s*$/i.test(before)
|
|
402
|
+
|| /^\s*(?:起|起价|起步|左右|上下|以上|\+)/i.test(after)) continue
|
|
403
|
+
prices.push({
|
|
404
|
+
amount: Number(amountText.replaceAll(',', '')),
|
|
405
|
+
currency: match[1] ? 'CNY' : match[2]!.toUpperCase(),
|
|
406
|
+
token: amountText,
|
|
407
|
+
start: match.index,
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
return prices
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
interface MalformedHardPrice {
|
|
414
|
+
currency: string
|
|
415
|
+
token: string
|
|
416
|
+
start: number
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 先识别完整 token 之外的畸形逗号数字;¥7xx 等既有非数字/模糊值不在此列。
|
|
421
|
+
*/
|
|
422
|
+
function malformedHardPricesInLine(text: string): MalformedHardPrice[] {
|
|
423
|
+
const amount = String.raw`(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?`
|
|
424
|
+
const token = String.raw`\d[\d,]*(?:\.\d+)?`
|
|
425
|
+
const pattern = new RegExp(String.raw`(?:¥\s*|\b([A-Z]{3})\s*)(${token})(?![\dA-Za-z])`, 'gi')
|
|
426
|
+
const malformed: MalformedHardPrice[] = []
|
|
427
|
+
for (const match of text.matchAll(pattern)) {
|
|
428
|
+
const raw = match[2]
|
|
429
|
+
if (!raw || !raw.includes(',') || new RegExp(`^(?:${amount})$`).test(raw)) continue
|
|
430
|
+
malformed.push({ currency: match[1] ? match[1].toUpperCase() : 'CNY', token: raw, start: match.index ?? 0 })
|
|
431
|
+
}
|
|
432
|
+
return malformed
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
interface MoneyField {
|
|
436
|
+
text: string
|
|
437
|
+
offset: number
|
|
438
|
+
index: number
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
interface AmbiguousHardPrice {
|
|
442
|
+
currency: string
|
|
443
|
+
token: string
|
|
444
|
+
start: number
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
interface FareRoleAnalysis {
|
|
448
|
+
prices: HardPrice[]
|
|
449
|
+
malformed: MalformedHardPrice[]
|
|
450
|
+
ambiguous: AmbiguousHardPrice[]
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function moneyFields(text: string): MoneyField[] {
|
|
454
|
+
const fields = text.split(/[;;|,]/)
|
|
455
|
+
const scopes: MoneyField[] = []
|
|
456
|
+
let offset = 0
|
|
457
|
+
for (const [index, field] of fields.entries()) {
|
|
458
|
+
scopes.push({ text: field, offset, index })
|
|
459
|
+
offset += field.length + 1
|
|
460
|
+
}
|
|
461
|
+
return scopes
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const EXPLICIT_FARE_LABEL = /(?:票价|机票价|fare|price)\s*[::]?\s*$/i
|
|
465
|
+
const PREVIOUS_MONEY_TOKEN = /(?:¥\s*|\b[A-Z]{3}\s*)\d[\d,]*(?:\.\d+)?\s*$/i
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 分类 money role,而不是把「同一行」当成 fare:
|
|
469
|
+
* canonical 是第一个 money token 所在的首字段;后续 token 只有显式
|
|
470
|
+
* fare label 才可比。其它有文字标签的字段忽略;裸 token 或前面只是
|
|
471
|
+
* 另一个金额的 token 无可靠归属,必须 fail closed。
|
|
472
|
+
*/
|
|
473
|
+
function analyzeFareRoles(text: string): FareRoleAnalysis {
|
|
474
|
+
const analysis: FareRoleAnalysis = { prices: [], malformed: [], ambiguous: [] }
|
|
475
|
+
for (const field of moneyFields(text)) {
|
|
476
|
+
const prices = hardPricesInLine(field.text)
|
|
477
|
+
const malformed = malformedHardPricesInLine(field.text)
|
|
478
|
+
const tokens = [
|
|
479
|
+
...prices.map(price => ({ kind: 'complete' as const, start: price.start, price, malformed: undefined })),
|
|
480
|
+
...malformed.map(item => ({ kind: 'malformed' as const, start: item.start, price: undefined, malformed: item })),
|
|
481
|
+
].sort((a, b) => a.start - b.start)
|
|
482
|
+
for (const [tokenIndex, token] of tokens.entries()) {
|
|
483
|
+
const prefix = field.text.slice(0, token.start).trim()
|
|
484
|
+
const canonical = field.index === 0 && tokenIndex === 0
|
|
485
|
+
const explicitFare = EXPLICIT_FARE_LABEL.test(prefix)
|
|
486
|
+
const reliableFare = canonical || explicitFare
|
|
487
|
+
if (reliableFare) {
|
|
488
|
+
if (token.price) analysis.prices.push({ ...token.price, start: token.price.start + field.offset })
|
|
489
|
+
if (token.malformed) analysis.malformed.push({ ...token.malformed, start: token.malformed.start + field.offset })
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
492
|
+
// A bare token, or a token immediately following another money token,
|
|
493
|
+
// is ambiguous; any other textual prefix accounts for a non-fare field.
|
|
494
|
+
if (prefix.length === 0 || PREVIOUS_MONEY_TOKEN.test(prefix)) {
|
|
495
|
+
const currency = token.price?.currency ?? token.malformed?.currency ?? 'CNY'
|
|
496
|
+
const raw = token.price?.token ?? token.malformed?.token ?? ''
|
|
497
|
+
analysis.ambiguous.push({ currency, token: raw, start: token.start + field.offset })
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return analysis
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function flightTrainPriceScope(text: string, fact: Extract<BookableFact, { kind: 'flight' | 'train' }>): string {
|
|
505
|
+
const no = fact.flight_no.toUpperCase()
|
|
506
|
+
const noStart = text.toUpperCase().indexOf(no)
|
|
507
|
+
if (noStart < 0) return ''
|
|
508
|
+
const afterNo = text.slice(noStart + no.length)
|
|
509
|
+
const nextFlight = afterNo.match(FLIGHT_NO)
|
|
510
|
+
const anchor = text.indexOf('<!-- fact:', noStart + no.length)
|
|
511
|
+
const evidence = afterNo.search(/\[[^\]\n]*#[^\]\n]*\]/)
|
|
512
|
+
let end = text.length
|
|
513
|
+
if (nextFlight?.index !== undefined) end = Math.min(end, noStart + no.length + nextFlight.index)
|
|
514
|
+
// The rendered fare is before its evidence chain; amounts after it are
|
|
515
|
+
// unrelated fees/budget text even when an anchor follows later on the line.
|
|
516
|
+
if (evidence >= 0) end = Math.min(end, noStart + no.length + evidence)
|
|
517
|
+
if (anchor >= 0) end = Math.min(end, anchor)
|
|
518
|
+
return text.slice(noStart, end)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function addPriceVerification(
|
|
522
|
+
violations: GateViolation[],
|
|
523
|
+
line: number,
|
|
524
|
+
fullText: string,
|
|
525
|
+
visibleText: string,
|
|
526
|
+
fact: Extract<BookableFact, { kind: 'flight' | 'train' }>,
|
|
527
|
+
): boolean {
|
|
528
|
+
const fullScope = flightTrainPriceScope(fullText, fact)
|
|
529
|
+
const visibleScope = flightTrainPriceScope(visibleText, fact)
|
|
530
|
+
const fullAnalysis = analyzeFareRoles(fullScope)
|
|
531
|
+
const visibleAnalysis = analyzeFareRoles(visibleScope)
|
|
532
|
+
const allPrices = fullAnalysis.prices
|
|
533
|
+
const visiblePrices = visibleAnalysis.prices
|
|
534
|
+
const hasAnchor = fullText.includes('<!-- fact:')
|
|
535
|
+
|
|
536
|
+
if (fullAnalysis.malformed.length > 0 || fullAnalysis.ambiguous.length > 0) {
|
|
537
|
+
const malformed = fullAnalysis.malformed[0]
|
|
538
|
+
const ambiguous = fullAnalysis.ambiguous[0]
|
|
539
|
+
violations.push({
|
|
540
|
+
kind: 'unverified_price_claim',
|
|
541
|
+
line,
|
|
542
|
+
detail: malformed
|
|
543
|
+
? `${fact.flight_no} fare role 含畸形硬价格 ${malformed.currency} ${malformed.token}——数字分组不完整,按未核验处理,不得截断或当作完整票价比较`
|
|
544
|
+
: `${fact.flight_no} 行内硬价格 ${ambiguous?.currency ?? 'CNY'} ${ambiguous?.token ?? ''} 无可靠 fare role 归属——按未核验处理,不得把裸金额当作 exact-date 票价`,
|
|
545
|
+
})
|
|
546
|
+
return true
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Heuristic claims retain the existing 120-character extraction window. If a
|
|
550
|
+
// hard price only appears outside it, it cannot be safely bound to this claim.
|
|
551
|
+
if (!hasAnchor && allPrices.some(p => p.start >= visibleScope.length)) {
|
|
552
|
+
violations.push({
|
|
553
|
+
kind: 'unverified_price_claim',
|
|
554
|
+
line,
|
|
555
|
+
detail: `${fact.flight_no} 行内硬价格未锚定且超出 120 字抽取窗口——无法作为 exact-date 事实核验,按未核验处理`,
|
|
556
|
+
})
|
|
557
|
+
return true
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (allPrices.length === 0) return false
|
|
561
|
+
const factCurrency = fact.currency?.toUpperCase()
|
|
562
|
+
for (const rendered of visiblePrices) {
|
|
563
|
+
if (fact.price === undefined || !Number.isFinite(fact.price) || factCurrency !== 'CNY') {
|
|
564
|
+
violations.push({
|
|
565
|
+
kind: 'unverified_price_claim',
|
|
566
|
+
line,
|
|
567
|
+
detail: `${fact.flight_no} 行内硬价格 ${rendered.currency} ${rendered.amount} 缺少可比较的权威 exact-date 事实价格——按未核验处理,不作汇率或币种猜测`,
|
|
568
|
+
})
|
|
569
|
+
return true
|
|
570
|
+
}
|
|
571
|
+
// ¥/CNY only compare against a CNY fact. No FX or ambiguous-currency guess.
|
|
572
|
+
if (rendered.currency !== factCurrency) {
|
|
573
|
+
violations.push({
|
|
574
|
+
kind: 'unverified_price_claim',
|
|
575
|
+
line,
|
|
576
|
+
detail: `${fact.flight_no} 行内硬价格 ${rendered.currency} ${rendered.amount} 不支持直接比较事实 ${factCurrency} ${fact.price}——按未核验处理,不作汇率换算`,
|
|
577
|
+
})
|
|
578
|
+
return true
|
|
579
|
+
}
|
|
580
|
+
if (rendered.amount === fact.price) continue
|
|
581
|
+
violations.push({
|
|
582
|
+
kind: 'price_contradicted',
|
|
583
|
+
line,
|
|
584
|
+
detail: `${fact.flight_no} 行内硬价格 ${rendered.currency} ${rendered.amount} ≠ exact-date 事实 ${factCurrency} ${fact.price}——价格事实矛盾,不得改写工具返回价格`,
|
|
585
|
+
})
|
|
586
|
+
return true
|
|
587
|
+
}
|
|
588
|
+
return false
|
|
589
|
+
}
|
|
590
|
+
|
|
278
591
|
export function gateArtifact(
|
|
279
592
|
markdown: string,
|
|
280
593
|
facts: BookableFact[],
|
|
281
594
|
map: AirlineAirportMap,
|
|
282
|
-
opts?: { trip_year?: number; itinerary?: ItineraryFacts },
|
|
595
|
+
opts?: { trip_year?: number; itinerary?: ItineraryFacts; tripStart?: string },
|
|
283
596
|
): GateReport {
|
|
284
597
|
const claims = extractClaims(markdown, map, opts)
|
|
285
598
|
const violations: GateViolation[] = []
|
|
@@ -287,16 +600,196 @@ export function gateArtifact(
|
|
|
287
600
|
|
|
288
601
|
// 渲染锚点优先(issue #118 单向生成):带 fact:<id> 的行确定性回溯注册表——
|
|
289
602
|
// 锚点在=按事实 bookability 判;锚点不存在=手改/伪造,直接违例。启发式对锚点行让位。
|
|
603
|
+
const lines = markdown.split('\n')
|
|
290
604
|
for (const [lineNo, factId] of claims.anchors) {
|
|
291
605
|
const f = facts.find(x => x.fact_id === factId)
|
|
292
606
|
if (!f) {
|
|
293
607
|
violations.push({ kind: 'fact_anchor_unknown', line: lineNo, detail: `渲染锚点 fact:${factId} 不在事实注册表——锚点被手改或伪造,产物不可信` })
|
|
294
608
|
continue
|
|
295
609
|
}
|
|
610
|
+
if (f.kind === 'train') {
|
|
611
|
+
const rendered = lines[lineNo - 1] ?? ''
|
|
612
|
+
const canonical = renderFlightFact(f)
|
|
613
|
+
if (rendered.trim() !== canonical.trim()) {
|
|
614
|
+
violations.push({
|
|
615
|
+
kind: 'fact_anchor_unknown',
|
|
616
|
+
line: lineNo,
|
|
617
|
+
detail: `train 锚点行与 canonical renderer 不一致——车次/时刻/日期/route 可能被手改;期望 ${canonical}`,
|
|
618
|
+
})
|
|
619
|
+
continue
|
|
620
|
+
}
|
|
621
|
+
const rail = railClaimVerdict(facts, {
|
|
622
|
+
flight_no: f.flight_no,
|
|
623
|
+
origin: f.route.origin,
|
|
624
|
+
destination: f.route.destination,
|
|
625
|
+
date: f.date,
|
|
626
|
+
})
|
|
627
|
+
if (rail.verdict !== 'traceable') {
|
|
628
|
+
violations.push({ kind: 'rail_claim_unverified', line: lineNo, detail: `${f.flight_no}: 锚点未通过 train exact-date 回溯(${rail.verdict})——${rail.reason}` })
|
|
629
|
+
continue
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// 航班锚点行字段指纹(issue #363,D-26 残余收口):锚点存在 ≠ 可见
|
|
633
|
+
// 内容与事实一致——flight_no 是「锚点 + 渲染原语」之间唯一可手改的
|
|
634
|
+
// 关键字段,保留 fact_id/anchor/registry 而改 flight_no(UO724→UO999)
|
|
635
|
+
// 仍会被「同号回溯」路径误判为 traceable,根反例验收正是此形态。
|
|
636
|
+
// 闸侧改用字段指纹:从渲染行重新抽 flight_no token,必须严格 ==
|
|
637
|
+
// 事实 flight_no。同一字段不引入 whole-line equality;承运/时刻/route/
|
|
638
|
+
// 日期/价格/打码段的兼容(§4b + §9 + §10 + §11)继续由既有判定
|
|
639
|
+
// 原语负责,不被新指纹吞并。同一行 trip-test 不命中即 fail-closed。
|
|
640
|
+
if (f.kind === 'flight') {
|
|
641
|
+
const rendered = lines[lineNo - 1] ?? ''
|
|
642
|
+
const canonicalFlightNo = f.flight_no.toUpperCase()
|
|
643
|
+
const renderedFlightNo = (() => {
|
|
644
|
+
for (const m of rendered.matchAll(FLIGHT_NO)) {
|
|
645
|
+
const code = m[1]?.toUpperCase()
|
|
646
|
+
if (code) return code
|
|
647
|
+
}
|
|
648
|
+
return undefined
|
|
649
|
+
})()
|
|
650
|
+
if (!renderedFlightNo || renderedFlightNo !== canonicalFlightNo) {
|
|
651
|
+
violations.push({
|
|
652
|
+
kind: 'fact_anchor_unknown',
|
|
653
|
+
line: lineNo,
|
|
654
|
+
detail: `flight 锚点行 flight_no 字段指纹不符——锚点存在但 visible flight_no 与事实 ${canonicalFlightNo} 不一致(渲染抽到 ${renderedFlightNo ?? '∅'});保留 fact_id/registry/anchor 改写可见航班号不被允许,期望行内含 ${canonicalFlightNo}`,
|
|
655
|
+
})
|
|
656
|
+
continue
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// 酒店锚点行字段指纹(issue #363,D-26 残余收口):destination + 档期
|
|
660
|
+
// 是「锚点 + 渲染原语」之间可手改的关键字段;保留 fact_id/anchor/
|
|
661
|
+
// registry 改写 目的地(大理→丽江)或 档期(2026-10-01→2026-12-25)
|
|
662
|
+
// 仍会被 hotelClaimVerdict 的同目的地同档期回溯判为 traceable,
|
|
663
|
+
// 根反例验收正是此形态。闸侧改为字段指纹:渲染行必须严格包含
|
|
664
|
+
// 事实的 destination(精确子串)+ check_in(精确子串)+ check_out
|
|
665
|
+
// (精确子串)三项;任一缺失或不一致 → fact_anchor_unknown fail-closed。
|
|
666
|
+
// 不引入 whole-line equality;价格打码/承运/时刻/政策的兼容形态
|
|
667
|
+
// 继续由既有判定原语负责。打码语义(options_masked = 9)与酒店事实
|
|
668
|
+
// 无 price 字段不变。
|
|
669
|
+
if (f.kind === 'hotel') {
|
|
670
|
+
const rendered = lines[lineNo - 1] ?? ''
|
|
671
|
+
const expectedDestination = f.destination
|
|
672
|
+
const expectedCheckIn = f.check_in
|
|
673
|
+
const expectedCheckOut = f.check_out
|
|
674
|
+
const missingDestination = !!expectedDestination && !rendered.includes(expectedDestination)
|
|
675
|
+
const missingCheckIn = !!expectedCheckIn && !rendered.includes(expectedCheckIn)
|
|
676
|
+
const missingCheckOut = !!expectedCheckOut && !rendered.includes(expectedCheckOut)
|
|
677
|
+
if (missingDestination || missingCheckIn || missingCheckOut) {
|
|
678
|
+
violations.push({
|
|
679
|
+
kind: 'fact_anchor_unknown',
|
|
680
|
+
line: lineNo,
|
|
681
|
+
detail: `hotel 锚点行字段指纹不符——锚点存在但 visible destination=${expectedDestination ?? '∅'}/check_in=${expectedCheckIn ?? '∅'}/check_out=${expectedCheckOut ?? '∅'} 与事实字段不严格一致(渲染行=${JSON.stringify(rendered)});保留 fact_id/registry/anchor 改写可见目的地或档期不被允许`,
|
|
682
|
+
})
|
|
683
|
+
continue
|
|
684
|
+
}
|
|
685
|
+
}
|
|
296
686
|
if (f.kind !== 'policy' && f.bookability === 'unavailable_exact_date') {
|
|
297
687
|
violations.push({ kind: 'not_in_source', line: lineNo, detail: `锚点事实为 exact-date 负事实(${(f as { fetched_at?: string }).fetched_at ?? ''})——负事实对应的可住/可订断言不得出现` })
|
|
298
688
|
continue
|
|
299
689
|
}
|
|
690
|
+
// 政策行内容指纹(issue #273 + #359,D-26 残余收口):
|
|
691
|
+
// 整行 = canonical body + ` <!-- fact:<id> -->`,单一权威面与 renderPolicyFact
|
|
692
|
+
// 共享 `policyCanonicalBody` 构造器;subject/statement/source/fetched_at/
|
|
693
|
+
// query_id/as_of 任一改动 → 行文本不再等于 canonical → fact_anchor_unknown
|
|
694
|
+
// fail-closed。锚点行不得含前导非空文本(允许锚点前的尾随空白)、第二个
|
|
695
|
+
// `<!-- fact:` 锚点、或任何「借合法锚点却写相反政策」的借用形态;行尾只允许
|
|
696
|
+
// `<!-- fact:<id> -->` 收尾。
|
|
697
|
+
//
|
|
698
|
+
// 兼容性回退(legacy public callers):当调用面未传 `opts.tripStart` 且未在
|
|
699
|
+
// itinerary 给出 `trip_start` 时,闸侧额外接受 renderer 产出的两个 canonical
|
|
700
|
+
// 形态之一(no-reminder 与 renderer-fixed 提醒 `;远期政策须复核——
|
|
701
|
+
// 到 YYYY-MM-DD 再核验一次`,日期为合法 ISO 阳历日)。这是 renderer 固定
|
|
702
|
+
// 短语+位置,不是任意 body 改写或后缀。该回退只匹配 renderer 自身输出,
|
|
703
|
+
// 不重新拼装、不猜 tripStart、不动 fact_id;有 `opts.tripStart` 或
|
|
704
|
+
// `itinerary.trip_start` 时优先严格比对对应 reminder,不退到回退集合。
|
|
705
|
+
if (f.kind === 'policy') {
|
|
706
|
+
const rendered = lines[lineNo - 1] ?? ''
|
|
707
|
+
// Precedence(issue #359 direct-API contract):explicit `opts.tripStart` →
|
|
708
|
+
// `opts.itinerary.trip_start` → undefined。policy 分支渲染上下文相关
|
|
709
|
+
// canonical 行(严格比对 + 结构失败诊断)全部走同一个
|
|
710
|
+
// `effectiveTripStart`,不再要求调用方在 `opts.tripStart` 与
|
|
711
|
+
// `itinerary.trip_start` 之间复制同一天。原来由 `index.ts` 桥接
|
|
712
|
+
// itinerary → tripStart 的注册路径仍然兼容(显式 tripStart 优先,语义不变)。
|
|
713
|
+
const explicitTripStart = typeof opts?.tripStart === 'string' && opts.tripStart.length > 0 ? opts.tripStart : undefined
|
|
714
|
+
const itineraryTripStart = typeof opts?.itinerary?.trip_start === 'string' && opts.itinerary.trip_start.length > 0
|
|
715
|
+
? opts.itinerary!.trip_start
|
|
716
|
+
: undefined
|
|
717
|
+
const effectiveTripStart: string | undefined = explicitTripStart ?? itineraryTripStart
|
|
718
|
+
const anchorRe = /<!-- fact:([0-9a-f]{16}) -->/
|
|
719
|
+
const m = rendered.match(anchorRe)
|
|
720
|
+
const noSecondAnchor = !m || m.index === undefined
|
|
721
|
+
? true
|
|
722
|
+
: !rendered.slice(0, m.index).includes('<!-- fact:')
|
|
723
|
+
const exactlyOneAnchor = m && m.index !== undefined && noSecondAnchor
|
|
724
|
+
const trailingNonEmpty = m && m.index !== undefined
|
|
725
|
+
? /\S/.test(rendered.slice(m.index + m[0]!.length))
|
|
726
|
+
: true
|
|
727
|
+
const exactAnchorId = m && m.index !== undefined && m[1] === f.fact_id
|
|
728
|
+
const structureOk = exactlyOneAnchor && exactAnchorId && !trailingNonEmpty
|
|
729
|
+
if (!structureOk) {
|
|
730
|
+
const canonical = renderPolicyFact(f, effectiveTripStart)
|
|
731
|
+
violations.push({
|
|
732
|
+
kind: 'fact_anchor_unknown',
|
|
733
|
+
line: lineNo,
|
|
734
|
+
detail: `政策锚点行结构不合法——必须为 canonical body + 单一「 <!-- fact:<id> -->」且行尾无后置非空文本(检测:行=${JSON.stringify(rendered)},期望=${JSON.stringify(canonical)});手改/伪造锚点或借用合法锚点写相反政策均 fail-closed`,
|
|
735
|
+
})
|
|
736
|
+
continue
|
|
737
|
+
}
|
|
738
|
+
const hasExplicitTripContext = effectiveTripStart !== undefined
|
|
739
|
+
if (!hasExplicitTripContext) {
|
|
740
|
+
// 兼容性回退(legacy public callers):仅当事实无 `review_by` 时,接受
|
|
741
|
+
// renderer 产出的两个固定 canonical 形态——不手动拆 body/provenance。
|
|
742
|
+
// (a) no-reminder 行 —— 严格等于 `renderPolicyFact(f)`。
|
|
743
|
+
// (b) reminder 行 —— 从 renderer 固定短语里**只抽日期**,把日期塞回
|
|
744
|
+
// `{...f, review_by: date}` 经 renderer 重建整行,与原行 byte-for-byte
|
|
745
|
+
// 比对;同时校验日期是真实 ISO 阳历日。这样 subject/statement/source/
|
|
746
|
+
// fetched_at/query_id/as_of、reminder 位置/短语、锚点结构、provenance
|
|
747
|
+
// 块全都由 renderer 重新产出并比对,不再人工拆段。
|
|
748
|
+
// 若事实自带 `review_by`,renderer 唯一合法 reminder 日期就是 `f.review_by`;
|
|
749
|
+
// (a) 已涵盖合法 pass;(b) 走 `{...f, review_by: extractedDate}` 时若
|
|
750
|
+
// extractedDate ≠ `f.review_by` 必因重建 ≠ 原行而 fall through,fail-closed。
|
|
751
|
+
if (rendered === renderPolicyFact(f)) {
|
|
752
|
+
traceable++
|
|
753
|
+
continue
|
|
754
|
+
}
|
|
755
|
+
if (typeof f.review_by !== 'string') {
|
|
756
|
+
const remMatch = rendered.match(POLICY_REMINDER_PHRASE_RE)
|
|
757
|
+
if (remMatch) {
|
|
758
|
+
const isoDate = remMatch[1]!
|
|
759
|
+
const realIso = (() => {
|
|
760
|
+
const dm = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate)
|
|
761
|
+
if (!dm) return false
|
|
762
|
+
const y = Number(dm[1]); const mo = Number(dm[2]); const da = Number(dm[3])
|
|
763
|
+
if (mo < 1 || mo > 12 || da < 1 || da > 31) return false
|
|
764
|
+
const test = new Date(Date.UTC(y, mo - 1, da))
|
|
765
|
+
return test.getUTCFullYear() === y && test.getUTCMonth() === mo - 1 && test.getUTCDate() === da
|
|
766
|
+
})()
|
|
767
|
+
if (realIso && rendered === renderPolicyFact({ ...f, review_by: isoDate })) {
|
|
768
|
+
traceable++
|
|
769
|
+
continue
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// 即使 trip 上下文已知,renderer 不接 tripStart 时也会产出 no-reminder canonical;
|
|
775
|
+
// 该形态与 `renderPolicyFact(f)` 严格相等,在此处也必须合法(renderer 的
|
|
776
|
+
// tripStart argument 是 optional)。否则闸会把「renderer 选择不带 reminder」
|
|
777
|
+
// 误判成内容指纹不符,误伤合法事实行。
|
|
778
|
+
if (rendered === renderPolicyFact(f)) {
|
|
779
|
+
traceable++
|
|
780
|
+
continue
|
|
781
|
+
}
|
|
782
|
+
const canonical = renderPolicyFact(f, effectiveTripStart)
|
|
783
|
+
if (rendered !== canonical) {
|
|
784
|
+
violations.push({
|
|
785
|
+
kind: 'fact_anchor_unknown',
|
|
786
|
+
line: lineNo,
|
|
787
|
+
detail: `政策锚点行内容指纹不符——期望 ${canonical},实际 ${rendered};subject/statement/source/fetched_at/query_id/as_of 任一不一致均 fail-closed`,
|
|
788
|
+
})
|
|
789
|
+
continue
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if ((f.kind === 'flight' || f.kind === 'train') && addPriceVerification(violations, lineNo, lines[lineNo - 1] ?? '', lines[lineNo - 1] ?? '', f)) continue
|
|
300
793
|
traceable++
|
|
301
794
|
}
|
|
302
795
|
|
|
@@ -305,6 +798,10 @@ export function gateArtifact(
|
|
|
305
798
|
if (claims.anchors.has(c.line)) continue
|
|
306
799
|
if (!c.destination) {
|
|
307
800
|
violations.push({ kind: 'unverifiable_hotel_claim', line: c.line, detail: `酒店可住断言缺目的地上下文,无法回溯 exact-date 事实——fail closed 按未核验处理` })
|
|
801
|
+
// 无目的地时也检查无条件 ✓:对未核验住宿仍不应打确定性标记
|
|
802
|
+
if (CHECK_MARK.test(c.text)) {
|
|
803
|
+
violations.push({ kind: 'unconditional_check', line: c.line, detail: '对未核验酒店使用无条件 ✓/✅——只有 bookable_exact_date 才允许确定性标记' })
|
|
804
|
+
}
|
|
308
805
|
continue
|
|
309
806
|
}
|
|
310
807
|
const r = hotelClaimVerdict(facts, { destination: c.destination, check_in: c.check_in, check_out: c.check_out })
|
|
@@ -333,7 +830,7 @@ export function gateArtifact(
|
|
|
333
830
|
&& (!c.date || f.date === c.date))
|
|
334
831
|
if (!hasCarrier) {
|
|
335
832
|
const scoped = c.origin && c.destination && c.date
|
|
336
|
-
? latestFactsForRouteDate(facts, c.origin, c.destination, c.date)
|
|
833
|
+
? latestFactsForRouteDate(facts, c.origin, c.destination, c.date, 'flight')
|
|
337
834
|
: []
|
|
338
835
|
violations.push({
|
|
339
836
|
kind: scoped.length > 0 ? 'not_in_source' : 'carrier_direct_unverified',
|
|
@@ -356,6 +853,7 @@ export function gateArtifact(
|
|
|
356
853
|
})
|
|
357
854
|
continue
|
|
358
855
|
}
|
|
856
|
+
if (r.fact && addPriceVerification(violations, c.line, lines[c.line - 1] ?? '', c.text, r.fact)) continue
|
|
359
857
|
traceable++
|
|
360
858
|
continue
|
|
361
859
|
}
|
|
@@ -366,6 +864,31 @@ export function gateArtifact(
|
|
|
366
864
|
}
|
|
367
865
|
}
|
|
368
866
|
|
|
867
|
+
// 车次 claim 独立于航班 claim:同 route/date 的航班事实不能证明车次存在,
|
|
868
|
+
// 也不能把缺失车次写成通用 not_in_source。只有 train kind 的结构化
|
|
869
|
+
// exact-date 事实才可回溯;不存在 renderRailFact 或 static schedule 降级。
|
|
870
|
+
for (const c of claims.trains) {
|
|
871
|
+
if (claims.anchors.has(c.line)) continue
|
|
872
|
+
const r = railClaimVerdict(facts, c)
|
|
873
|
+
if (r.verdict === 'traceable') {
|
|
874
|
+
if (lineTimesContradict(c.text, r.fact?.dep_local, r.fact?.arr_local)) {
|
|
875
|
+
violations.push({
|
|
876
|
+
kind: 'rail_claim_unverified',
|
|
877
|
+
line: c.line,
|
|
878
|
+
detail: `${c.flight_no} 时刻与 train exact-date 快照不符(快照 ${r.fact?.dep_local}→${r.fact?.arr_local} ${r.fact?.date})——不得改写结构化车次事实`,
|
|
879
|
+
})
|
|
880
|
+
continue
|
|
881
|
+
}
|
|
882
|
+
if (r.fact && addPriceVerification(violations, c.line, lines[c.line - 1] ?? '', c.text, r.fact)) continue
|
|
883
|
+
traceable++
|
|
884
|
+
continue
|
|
885
|
+
}
|
|
886
|
+
violations.push({ kind: 'rail_claim_unverified', line: c.line, detail: `${c.flight_no}: ${r.verdict}——${r.reason}` })
|
|
887
|
+
if (CHECK_MARK.test(c.text)) {
|
|
888
|
+
violations.push({ kind: 'unconditional_check', line: c.line, detail: `对未核验车次 ${c.flight_no} 使用无条件 ✓/✅——只有 train bookable_exact_date 才允许确定性标记` })
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
369
892
|
// 通用直飞规则:该行未点名承运时,要求该 route+date 存在任一在架直飞;
|
|
370
893
|
// 「该日可售仅有中转/0 条」却写直飞 = 无证据分支(issue #46「DMK→KMG 可考虑 8L 直飞」行同款)
|
|
371
894
|
for (const d of claims.direct_lines) {
|
|
@@ -374,7 +897,7 @@ export function gateArtifact(
|
|
|
374
897
|
violations.push({ kind: 'route_unqueried', line: d.line, detail: `直飞断言缺 route/date 上下文,无法回溯——fail closed 按未核验处理` })
|
|
375
898
|
continue
|
|
376
899
|
}
|
|
377
|
-
const scoped = latestFactsForRouteDate(facts, d.origin, d.destination, d.date)
|
|
900
|
+
const scoped = latestFactsForRouteDate(facts, d.origin, d.destination, d.date, 'flight')
|
|
378
901
|
if (scoped.length === 0) {
|
|
379
902
|
violations.push({ kind: 'route_unqueried', line: d.line, detail: `${d.origin}→${d.destination} ${d.date} 的直飞断言从未经 exact-date 源核验` })
|
|
380
903
|
continue
|
|
@@ -408,11 +931,34 @@ export function gateArtifact(
|
|
|
408
931
|
}
|
|
409
932
|
}
|
|
410
933
|
|
|
934
|
+
// 词表外下单措辞 fail-closed(D-26 R1,issue #381,接管 #273 公开跟踪):
|
|
935
|
+
// 根反例(main 6e85d36)「普吉岛入境章费用 800元,现场支付」全零抽取静默过闸。
|
|
936
|
+
// 兜底词网:仅当(1)该行经全部既有抽取原语(锚点/航班/车次/政策/酒店/机场映射/
|
|
937
|
+
// 直飞)后零 claim,且(2)行内同时命中交易承诺短语与词表外金额写法,才按既有
|
|
938
|
+
// unverified_price_claim 显式拒绝——不发明新违例通道。已有 claim 的行由各自
|
|
939
|
+
// 既有机制负责,词网不重复计违例;heading 行与抽取面一致不入网。
|
|
940
|
+
const claimedLines = new Set<number>(claims.anchors.keys())
|
|
941
|
+
for (const c of [...claims.flights, ...claims.trains, ...claims.policies, ...claims.hotels, ...claims.airports, ...claims.direct_lines]) claimedLines.add(c.line)
|
|
942
|
+
lines.forEach((line, idx) => {
|
|
943
|
+
const lineNo = idx + 1
|
|
944
|
+
if (claimedLines.has(lineNo) || HEADING_ATX.test(line)) return
|
|
945
|
+
if (COMMITMENT_TRANSACTION_RE.test(line) && OFF_VOCAB_AMOUNT_RE.test(line)) {
|
|
946
|
+
violations.push({
|
|
947
|
+
kind: 'unverified_price_claim',
|
|
948
|
+
line: lineNo,
|
|
949
|
+
detail: '词表外下单措辞 fail-closed:该行含交易承诺措辞且金额为受支持币种词表(¥/ISO 前缀)外的写法,无 exact-date 事实可回溯——按未核验价格处理,不得静默过闸;改用渲染原语锚点或附「截至 YYYY-MM-DD」可溯来源(D-26 R1,issue #381)',
|
|
950
|
+
})
|
|
951
|
+
}
|
|
952
|
+
})
|
|
953
|
+
|
|
411
954
|
// 联程措辞:含「联程」的行,逐航班号查 protected_connection——
|
|
412
955
|
// 只有全部航腿同票保护才允许称联程;分票/自助转机必须显式标红
|
|
413
956
|
markdown.split('\n').forEach((line, i) => {
|
|
414
957
|
if (!line.includes('联程')) return
|
|
415
|
-
const
|
|
958
|
+
const trainNos = new Set([...line.matchAll(TRAIN_NO)].map(m => m[1]!.toUpperCase()))
|
|
959
|
+
const nos = [...line.matchAll(FLIGHT_NO)]
|
|
960
|
+
.map(m => m[1]!.toUpperCase())
|
|
961
|
+
.filter(no => !trainNos.has(no))
|
|
416
962
|
const matched = facts.filter((f): f is Extract<BookableFact, { kind: 'flight' | 'train' }> =>
|
|
417
963
|
(f.kind === 'flight' || f.kind === 'train') && nos.includes(f.flight_no.toUpperCase()))
|
|
418
964
|
const allProtected = nos.length > 0
|
|
@@ -437,7 +983,7 @@ export function gateArtifact(
|
|
|
437
983
|
return {
|
|
438
984
|
schema: ARTIFACT_GATE_SCHEMA,
|
|
439
985
|
verdict: violations.length > 0 ? 'blocked' : 'pass',
|
|
440
|
-
claims_checked: claims.flights.length + claims.policies.length + claims.airports.length,
|
|
986
|
+
claims_checked: claims.flights.length + claims.trains.length + claims.policies.length + claims.airports.length,
|
|
441
987
|
traceable,
|
|
442
988
|
violations,
|
|
443
989
|
presentation: violations.length > 0 ? 'verified_label_forbidden' : 'verified_itinerary_allowed',
|