@danceiny/gotry 0.0.1-rc.14 → 0.0.1-rc.16
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 +26 -20
- package/README.zh-CN.md +27 -21
- package/bin/gotry-bootstrap.js +331 -3
- package/cordis.gotry-patch.yml +10 -0
- 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 +217 -0
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +1 -1
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- 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 +345 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +377 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +77 -1
- package/dist/capabilities/session-search.js +69 -2
- package/dist/capabilities/weather.js +1 -1
- package/dist/scripts/action-cache-tests.js +1 -1
- 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/booking-saga-tests.js +187 -0
- package/dist/scripts/bootstrap-tests.js +49 -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/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/extension-tests.js +468 -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/nightly-evidence-tests.js +186 -0
- package/dist/scripts/nightly-evidence.js +289 -0
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +218 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- 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 +1 -1
- 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 +1 -1
- 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.js +159 -1
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- 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 +1 -1
- package/dist/scripts/wizard-bootstrap.js +32 -0
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +153 -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 +31 -7
- package/dist/src/engine.js +1 -1
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +322 -46
- 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 +1 -1
- package/dist/src/time-anchor.js +1 -1
- 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 +26 -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 +8 -2
- package/ts/capabilities/artifacts.ts +235 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +354 -0
- package/ts/capabilities/session/extension-channel.ts +92 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/wizard.ts +282 -0
- package/ts/capabilities/session-login.ts +53 -2
- package/ts/capabilities/session-search.ts +68 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/bookable-facts.ts +557 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/dsh-llm.ts +40 -3
- package/ts/src/index.ts +230 -28
package/dist/scripts/smoke.js
CHANGED
|
@@ -410,6 +410,164 @@ async function main() {
|
|
|
410
410
|
}
|
|
411
411
|
console.log('login tool: registered; 登录=外部网站+用户自己的浏览器,gotry 只读票据名(0 值过手)语义钉死');
|
|
412
412
|
}
|
|
413
|
+
{
|
|
414
|
+
const { writeFileSync } = await import('node:fs');
|
|
415
|
+
const { listArtifacts, readArtifact } = await import('../capabilities/artifacts.js');
|
|
416
|
+
const { ensureLedger } = await import('../src/state-ledger.js');
|
|
417
|
+
const cwdDir = mkdtempSync(join(tmpdir(), 'gotry-artifacts-cwd-'));
|
|
418
|
+
const twelve = Array.from({
|
|
419
|
+
length: 12
|
|
420
|
+
}, (_, i)=>`第 ${i + 1} 行`).join('\n');
|
|
421
|
+
writeFileSync(join(cwdDir, 'trip-2027-probe.md'), `# 行程·产物探针\n${twelve}\n`);
|
|
422
|
+
const ledger = ensureLedger(smokeRoot);
|
|
423
|
+
ledger.createWorkflowRun({
|
|
424
|
+
id: 'art-probe-1',
|
|
425
|
+
goal: '行程·产物探针(账本)',
|
|
426
|
+
ticket: {
|
|
427
|
+
objective: 'probe'
|
|
428
|
+
},
|
|
429
|
+
state: {}
|
|
430
|
+
});
|
|
431
|
+
ledger.settleWorkflowRun('art-probe-1', '# 交付·账本权威\nD1 大理\nD2 洱海');
|
|
432
|
+
const listTool = byName('gotry_artifacts_list');
|
|
433
|
+
const ledList = await listTool.execute({
|
|
434
|
+
query: {}
|
|
435
|
+
}, null);
|
|
436
|
+
const run = ledList.artifacts?.find((a)=>a.id === 'art-probe-1');
|
|
437
|
+
if (!ledList.ok || !run || run.source !== 'async-run' || run.status !== 'settled') {
|
|
438
|
+
throw new Error(`FAIL: artifacts list 应发现账本已交付工单,实际:${JSON.stringify(ledList).slice(0, 200)}`);
|
|
439
|
+
}
|
|
440
|
+
const direct = await listArtifacts({
|
|
441
|
+
stateRoot: smokeRoot,
|
|
442
|
+
cwd: cwdDir
|
|
443
|
+
});
|
|
444
|
+
if (!direct.artifacts.some((a)=>a.source === 'cwd-file' && a.id === 'trip-2027-probe.md')) {
|
|
445
|
+
throw new Error(`FAIL: artifacts list 应发现 cwd 顶层 md,实际:${JSON.stringify(direct.artifacts.map((a)=>a.id))}`);
|
|
446
|
+
}
|
|
447
|
+
const readTool = byName('gotry_artifacts_read');
|
|
448
|
+
const r1 = await readTool.execute({
|
|
449
|
+
query: {
|
|
450
|
+
path: 'art-probe-1'
|
|
451
|
+
}
|
|
452
|
+
}, null);
|
|
453
|
+
if (!r1.ok || r1.lines?.[0]?.number !== 1 || !r1.lines?.[0]?.text.includes('交付·账本权威') || r1.lang !== 'markdown') {
|
|
454
|
+
throw new Error(`FAIL: 裸工单 id 应从账本读出行号视图,实际:${JSON.stringify(r1).slice(0, 200)}`);
|
|
455
|
+
}
|
|
456
|
+
const view = readTool.presentResult?.({
|
|
457
|
+
query: {
|
|
458
|
+
path: 'art-probe-1'
|
|
459
|
+
}
|
|
460
|
+
}, r1);
|
|
461
|
+
if (view?.card !== 'read' || view.path !== r1.path || view.offset !== 1 || view.lines?.length !== r1.lines?.length || view.totalLines !== r1.totalLines || view.lang !== 'markdown') {
|
|
462
|
+
throw new Error(`FAIL: read 卡字段不齐,实际:${JSON.stringify(view).slice(0, 200)}`);
|
|
463
|
+
}
|
|
464
|
+
const r2 = await readArtifact({
|
|
465
|
+
stateRoot: smokeRoot,
|
|
466
|
+
cwd: cwdDir,
|
|
467
|
+
path: 'trip-2027-probe.md',
|
|
468
|
+
offset: 10,
|
|
469
|
+
limit: 5
|
|
470
|
+
});
|
|
471
|
+
if (!r2.ok || r2.lines[0].number !== 10 || r2.windowed !== false) {
|
|
472
|
+
throw new Error(`FAIL: 行窗口 12 行文件 offset=10 应从行号 10 起,实际:${JSON.stringify(r2.ok ? {
|
|
473
|
+
o: r2.offset,
|
|
474
|
+
first: r2.lines[0]?.number
|
|
475
|
+
} : r2)}`);
|
|
476
|
+
}
|
|
477
|
+
if (r2.lines.length !== 5) throw new Error(`FAIL: 14 行文件 offset=10/limit=5 窗口应恰 5 行(10-14),实际 ${r2.lines.length}`);
|
|
478
|
+
const badPath = await readArtifact({
|
|
479
|
+
stateRoot: smokeRoot,
|
|
480
|
+
cwd: cwdDir,
|
|
481
|
+
path: '../../../../../etc/passwd'
|
|
482
|
+
});
|
|
483
|
+
if (badPath.ok) throw new Error('FAIL: 越界路径必须被拒');
|
|
484
|
+
const badExt = await readArtifact({
|
|
485
|
+
stateRoot: smokeRoot,
|
|
486
|
+
cwd: cwdDir,
|
|
487
|
+
path: 'gotry-state/gotry-state.db'
|
|
488
|
+
});
|
|
489
|
+
if (badExt.ok) throw new Error('FAIL: 白名单外扩展名(.db)必须被拒');
|
|
490
|
+
rmSync(cwdDir, {
|
|
491
|
+
recursive: true,
|
|
492
|
+
force: true
|
|
493
|
+
});
|
|
494
|
+
console.log(`artifacts: ledger run + cwd md discovered; read window(${r2.ok ? r2.lines.length : '?'} lines @10) + read card; path/ext guardrails hold`);
|
|
495
|
+
}
|
|
496
|
+
{
|
|
497
|
+
const { appendFacts, loadFactRegistry } = await import('../capabilities/fact-log.js');
|
|
498
|
+
const { factsFromFlyai } = await import('../src/bookable-facts.js');
|
|
499
|
+
const avMap = JSON.parse(await readFile(join(import.meta.dirname, '..', '..', 'data', 'airline-airports.json'), 'utf-8'));
|
|
500
|
+
const fetchedAt = '2026-08-29T00:00:00.000Z';
|
|
501
|
+
await appendFacts(smokeRoot, [
|
|
502
|
+
...factsFromFlyai({
|
|
503
|
+
kind: 'flight',
|
|
504
|
+
origin: '香港',
|
|
505
|
+
destination: '普吉',
|
|
506
|
+
date: '2027-07-18'
|
|
507
|
+
}, {
|
|
508
|
+
verdict: 'hit',
|
|
509
|
+
options: [
|
|
510
|
+
{
|
|
511
|
+
no: 'CX771',
|
|
512
|
+
name: '国泰航空',
|
|
513
|
+
depDateTime: '2027-07-18T08:05:00+08:00',
|
|
514
|
+
arrDateTime: '2027-07-18T10:35:00+07:00',
|
|
515
|
+
price: 1234
|
|
516
|
+
}
|
|
517
|
+
],
|
|
518
|
+
evidence: ''
|
|
519
|
+
}, fetchedAt, avMap.city_alias),
|
|
520
|
+
...factsFromFlyai({
|
|
521
|
+
kind: 'flight',
|
|
522
|
+
origin: '深圳',
|
|
523
|
+
destination: '普吉',
|
|
524
|
+
date: '2027-07-16'
|
|
525
|
+
}, {
|
|
526
|
+
verdict: 'miss',
|
|
527
|
+
options: [],
|
|
528
|
+
evidence: ''
|
|
529
|
+
}, fetchedAt, avMap.city_alias)
|
|
530
|
+
]);
|
|
531
|
+
const registry = await loadFactRegistry(smokeRoot);
|
|
532
|
+
const probeFacts = registry.filter((f)=>f.kind !== 'policy' && (f.query_id.endsWith(':2027-07-16') || f.query_id.endsWith(':2027-07-18')));
|
|
533
|
+
if (probeFacts.length !== 2 || !probeFacts.some((f)=>f.bookability === 'unavailable_exact_date') || !probeFacts.some((f)=>f.bookability === 'bookable_exact_date')) {
|
|
534
|
+
throw new Error(`FAIL: 事实侧车应落 1 正 1 负两条,实际:${JSON.stringify(probeFacts.map((f)=>f.bookability))}`);
|
|
535
|
+
}
|
|
536
|
+
const gate = byName('gotry_fact_gate');
|
|
537
|
+
const passMd = [
|
|
538
|
+
'# 行程片段',
|
|
539
|
+
'## D3 7.18 香港 → 普吉',
|
|
540
|
+
'- 国泰航空 CX771 08:05→10:35(已按 exact-date 检索记录)'
|
|
541
|
+
].join('\n');
|
|
542
|
+
const pass = await gate.execute({
|
|
543
|
+
query: {
|
|
544
|
+
markdown: passMd,
|
|
545
|
+
tripYear: 2027
|
|
546
|
+
}
|
|
547
|
+
}, null);
|
|
548
|
+
if (pass.verdict !== 'pass' || pass.presentation !== 'verified_itinerary_allowed') {
|
|
549
|
+
throw new Error(`FAIL: 可回溯产物应 pass,实际:${JSON.stringify(pass).slice(0, 300)}`);
|
|
550
|
+
}
|
|
551
|
+
const badMd = [
|
|
552
|
+
'# 行程片段',
|
|
553
|
+
'## D1 7.16 深圳 → 普吉',
|
|
554
|
+
'- 香港快运 UO784 10:05→12:40 直飞 ✓'
|
|
555
|
+
].join('\n');
|
|
556
|
+
const blocked = await gate.execute({
|
|
557
|
+
query: {
|
|
558
|
+
markdown: badMd,
|
|
559
|
+
tripYear: 2027
|
|
560
|
+
}
|
|
561
|
+
}, null);
|
|
562
|
+
if (blocked.verdict !== 'blocked' || !blocked.violations?.some((v)=>v.kind === 'not_in_source')) {
|
|
563
|
+
throw new Error(`FAIL: exact-date miss 被 UO784 填充必须 blocked(not_in_source),实际:${JSON.stringify(blocked).slice(0, 300)}`);
|
|
564
|
+
}
|
|
565
|
+
const view = gate.presentResult?.({
|
|
566
|
+
query: {}
|
|
567
|
+
}, blocked);
|
|
568
|
+
if (!view?.title?.includes('blocked')) throw new Error(`FAIL: blocked 呈现卡标题应含 blocked,实际:${view?.title}`);
|
|
569
|
+
console.log(`fact gate: registry 1+1(hit/miss);verified 措辞 pass;miss 填充 UO784 blocked(not_in_source) + 呈现卡`);
|
|
570
|
+
}
|
|
413
571
|
rmSync(smokeRoot, {
|
|
414
572
|
recursive: true,
|
|
415
573
|
force: true
|
|
@@ -423,4 +581,4 @@ main().catch((e)=>{
|
|
|
423
581
|
});
|
|
424
582
|
|
|
425
583
|
|
|
426
|
-
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/smoke.ts
|
|
584
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/smoke.ts
|
|
@@ -120,4 +120,4 @@ pass('trip_id 语义派生:确定性可重放', ()=>{
|
|
|
120
120
|
console.log(`\nTRAVEL TIMELINE TESTS: ${n}/7 OK(memory-design P1 守门面)`);
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/travel-timeline-tests.ts
|
|
123
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/travel-timeline-tests.ts
|
|
@@ -46,4 +46,4 @@ assert.deepEqual(r4.work_window_exclusions, []);
|
|
|
46
46
|
console.log(`TS UNIFIED TESTS: 4/4 OK(工作窗口生效,f2=VZ303,¥${r1.money_cny})`);
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/unified-tests.ts
|
|
49
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/unified-tests.ts
|
|
@@ -52,4 +52,4 @@ console.log('5. WMO 码映射 OK');
|
|
|
52
52
|
console.log('\nWEATHER TESTS: 6/6 OK(Open-Meteo 真实 API,免费无 key;普吉岛走 Nominatim 兜底层)');
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/weather-tests.ts
|
|
55
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/weather-tests.ts
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { runOnboardingWizard } from '../capabilities/session/wizard.js';
|
|
3
|
+
function parseArgs(argv) {
|
|
4
|
+
const args = {};
|
|
5
|
+
for(let i = 0; i < argv.length; i += 1){
|
|
6
|
+
const cur = argv[i];
|
|
7
|
+
if (cur === '--extension-dir') args.extensionDir = argv[++i];
|
|
8
|
+
else if (cur === '--source-dir') args.sourceDir = argv[++i];
|
|
9
|
+
}
|
|
10
|
+
return args;
|
|
11
|
+
}
|
|
12
|
+
async function main() {
|
|
13
|
+
const args = parseArgs(process.argv.slice(2));
|
|
14
|
+
const result = await runOnboardingWizard({
|
|
15
|
+
dryRun: false,
|
|
16
|
+
...args.extensionDir ? {
|
|
17
|
+
extensionDir: args.extensionDir
|
|
18
|
+
} : {},
|
|
19
|
+
...args.sourceDir ? {
|
|
20
|
+
sourceDir: args.sourceDir
|
|
21
|
+
} : {}
|
|
22
|
+
});
|
|
23
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
main().catch((e)=>{
|
|
27
|
+
process.stderr.write(`[wizard-bootstrap] error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/wizard-bootstrap.ts
|
|
@@ -32,4 +32,4 @@ for(let round = 0; round < ROUNDS; round++){
|
|
|
32
32
|
console.log(`Z3 RACE TESTS OK(进程内三形态并发 ×${ROUNDS}:判定与顺序基线一致,零 WASM 降级)`);
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/z3-race-tests.ts
|
|
35
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/z3-race-tests.ts
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { flightClaimVerdict, itineraryInvariants, latestFactsForRouteDate } from './bookable-facts.js';
|
|
2
|
+
export const ARTIFACT_GATE_SCHEMA = 'gotry_artifact_gate.v1';
|
|
3
|
+
const FLIGHT_NO = /(?<![A-Za-z0-9])([A-Z0-9]{2}\d{3,4})(?![\d])/g;
|
|
4
|
+
const CARRIER_DIRECT = /(?<![A-Za-z0-9])([A-Z0-9]{2})(?!\d)(?:\s|[一-龥]){0,6}直飞/g;
|
|
5
|
+
const CARRIER_ZH = {
|
|
6
|
+
东航: 'MU',
|
|
7
|
+
南航: 'CZ',
|
|
8
|
+
国航: 'CA',
|
|
9
|
+
厦航: 'MF',
|
|
10
|
+
春秋: '9C',
|
|
11
|
+
祥鹏: '8L',
|
|
12
|
+
亚航: 'FD',
|
|
13
|
+
越捷: 'VZ',
|
|
14
|
+
国泰: 'CX',
|
|
15
|
+
港航: 'HX',
|
|
16
|
+
香港快运: 'UO',
|
|
17
|
+
阿联酋: 'EK',
|
|
18
|
+
泰航: 'TG'
|
|
19
|
+
};
|
|
20
|
+
const POLICY_WORD = /免签|落地签|签证|入境申报|过境免/;
|
|
21
|
+
const AS_OF_WORD = /截至\s*\d{4}[-/年]\d{1,2}|as[_ ]?of\s*\d{4}/i;
|
|
22
|
+
const CHECK_MARK = /[✓✅]/;
|
|
23
|
+
function hasDirectAssertion(line) {
|
|
24
|
+
const idx = line.indexOf('直飞');
|
|
25
|
+
if (idx < 0) return false;
|
|
26
|
+
const before = line.slice(Math.max(0, idx - 3), idx);
|
|
27
|
+
return !(before.endsWith('无') || before.endsWith('不') || before.endsWith('没有') || before.endsWith('未见'));
|
|
28
|
+
}
|
|
29
|
+
function routeCtxOf(text, map, defaultYear) {
|
|
30
|
+
const occ = [];
|
|
31
|
+
for (const [city, iata] of Object.entries(map.city_alias)){
|
|
32
|
+
let pos = text.indexOf(city);
|
|
33
|
+
while(pos >= 0){
|
|
34
|
+
occ.push({
|
|
35
|
+
city,
|
|
36
|
+
iata,
|
|
37
|
+
pos
|
|
38
|
+
});
|
|
39
|
+
pos = text.indexOf(city, pos + city.length);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
occ.sort((a, b)=>a.pos - b.pos);
|
|
43
|
+
const ctx = {};
|
|
44
|
+
outer: for(let i = 0; i < occ.length; i++){
|
|
45
|
+
for(let j = i + 1; j < occ.length; j++){
|
|
46
|
+
if (occ[j].iata === occ[i].iata) continue;
|
|
47
|
+
const between = text.slice(occ[i].pos + occ[i].city.length, occ[j].pos);
|
|
48
|
+
if (/→|⇀|->/.test(between)) {
|
|
49
|
+
ctx.origin = occ[i].iata;
|
|
50
|
+
ctx.destination = occ[j].iata;
|
|
51
|
+
break outer;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!ctx.origin && occ.length >= 2) {
|
|
56
|
+
ctx.origin = occ[0].iata;
|
|
57
|
+
ctx.destination = occ[1].iata;
|
|
58
|
+
}
|
|
59
|
+
if (defaultYear) {
|
|
60
|
+
const dm = text.match(/(?<!\d)(\d{1,2})\.(\d{1,2})(?!\d)/) ?? text.match(/(?<!\d)(\d{2})-(\d{2})(?!\d)/);
|
|
61
|
+
if (dm) {
|
|
62
|
+
const mm = Number(dm[1]), dd = Number(dm[2]);
|
|
63
|
+
if (mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31) {
|
|
64
|
+
ctx.date = `${defaultYear}-${String(mm).padStart(2, '0')}-${String(dd).padStart(2, '0')}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return ctx;
|
|
69
|
+
}
|
|
70
|
+
export function extractClaims(markdown, map, opts) {
|
|
71
|
+
const claims = {
|
|
72
|
+
flights: [],
|
|
73
|
+
policies: [],
|
|
74
|
+
airports: [],
|
|
75
|
+
direct_lines: []
|
|
76
|
+
};
|
|
77
|
+
const lines = markdown.split('\n');
|
|
78
|
+
let section = {};
|
|
79
|
+
for(let i = 0; i < lines.length; i++){
|
|
80
|
+
const line = lines[i];
|
|
81
|
+
const lineNo = i + 1;
|
|
82
|
+
if (/^#{1,4}\s/.test(line)) {
|
|
83
|
+
const ctx = routeCtxOf(line, map, opts?.trip_year);
|
|
84
|
+
section = {
|
|
85
|
+
origin: ctx.origin,
|
|
86
|
+
destination: ctx.destination,
|
|
87
|
+
date: ctx.date ?? section.date
|
|
88
|
+
};
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const own = routeCtxOf(line, map, opts?.trip_year);
|
|
92
|
+
const origin = own.origin ?? section.origin;
|
|
93
|
+
const destination = own.destination ?? section.destination;
|
|
94
|
+
const date = own.date ?? section.date;
|
|
95
|
+
for (const m of line.matchAll(FLIGHT_NO)){
|
|
96
|
+
claims.flights.push({
|
|
97
|
+
line: lineNo,
|
|
98
|
+
text: line.trim().slice(0, 120),
|
|
99
|
+
flight_no: m[1].toUpperCase(),
|
|
100
|
+
origin,
|
|
101
|
+
destination,
|
|
102
|
+
date
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
for (const m of line.matchAll(CARRIER_DIRECT)){
|
|
106
|
+
const carrier = m[1].toUpperCase();
|
|
107
|
+
if (!/[A-Z]/.test(carrier)) continue;
|
|
108
|
+
if (new RegExp(`${carrier}\\d`).test(line)) continue;
|
|
109
|
+
claims.flights.push({
|
|
110
|
+
line: lineNo,
|
|
111
|
+
text: line.trim().slice(0, 120),
|
|
112
|
+
flight_no: '',
|
|
113
|
+
carrier_only: carrier,
|
|
114
|
+
origin,
|
|
115
|
+
destination,
|
|
116
|
+
date
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (hasDirectAssertion(line) && (origin || destination)) {
|
|
120
|
+
claims.direct_lines.push({
|
|
121
|
+
line: lineNo,
|
|
122
|
+
text: line.trim().slice(0, 120),
|
|
123
|
+
origin,
|
|
124
|
+
destination,
|
|
125
|
+
date
|
|
126
|
+
});
|
|
127
|
+
for (const [zh, code] of Object.entries(CARRIER_ZH)){
|
|
128
|
+
if (new RegExp(`(?<![一-龥])${zh}`).test(line) && !new RegExp(`${code}\\d`).test(line) && !claims.flights.some((c)=>c.line === lineNo && c.carrier_only === code)) {
|
|
129
|
+
claims.flights.push({
|
|
130
|
+
line: lineNo,
|
|
131
|
+
text: line.trim().slice(0, 120),
|
|
132
|
+
flight_no: '',
|
|
133
|
+
carrier_only: code,
|
|
134
|
+
origin,
|
|
135
|
+
destination,
|
|
136
|
+
date
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (POLICY_WORD.test(line)) {
|
|
142
|
+
claims.policies.push({
|
|
143
|
+
line: lineNo,
|
|
144
|
+
text: line.trim().slice(0, 160),
|
|
145
|
+
has_as_of: AS_OF_WORD.test(line)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
for (const [city, carriers] of Object.entries(map.carrier_airport)){
|
|
149
|
+
const airports = [
|
|
150
|
+
...new Set(Object.values(carriers))
|
|
151
|
+
];
|
|
152
|
+
if (!airports.some((a)=>line.includes(a))) continue;
|
|
153
|
+
const mentioned = Object.keys(carriers).filter((c)=>new RegExp(`(?<![A-Za-z0-9])${c}(?![A-Za-z0-9])`).test(line));
|
|
154
|
+
if (mentioned.length === 0) continue;
|
|
155
|
+
const airport = airports.find((a)=>line.includes(a));
|
|
156
|
+
if (airport) claims.airports.push({
|
|
157
|
+
line: lineNo,
|
|
158
|
+
text: line.trim().slice(0, 120),
|
|
159
|
+
carriers: mentioned,
|
|
160
|
+
airport,
|
|
161
|
+
city
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return claims;
|
|
166
|
+
}
|
|
167
|
+
function lineTimesContradict(text, dep, arr) {
|
|
168
|
+
if (!dep && !arr) return false;
|
|
169
|
+
const times = [
|
|
170
|
+
...text.matchAll(/(?<!\d)(\d{1,2}:\d{2})(?!\d)/g)
|
|
171
|
+
].map((m)=>m[1]);
|
|
172
|
+
if (times.length === 0) return false;
|
|
173
|
+
return !times.some((t)=>t === dep || t === arr);
|
|
174
|
+
}
|
|
175
|
+
export function gateArtifact(markdown, facts, map, opts) {
|
|
176
|
+
const claims = extractClaims(markdown, map, opts);
|
|
177
|
+
const violations = [];
|
|
178
|
+
let traceable = 0;
|
|
179
|
+
for (const c of claims.flights){
|
|
180
|
+
if (c.carrier_only) {
|
|
181
|
+
const hasCarrier = facts.some((f)=>f.kind === 'flight' && f.bookability === 'bookable_exact_date' && f.nonstop !== false && f.flight_no.startsWith(c.carrier_only) && (!c.origin || f.route.origin === c.origin || f.route.origin_airport === c.origin) && (!c.destination || f.route.destination === c.destination || f.route.dest_airport === c.destination) && (!c.date || f.date === c.date));
|
|
182
|
+
if (!hasCarrier) {
|
|
183
|
+
const scoped = c.origin && c.destination && c.date ? latestFactsForRouteDate(facts, c.origin, c.destination, c.date) : [];
|
|
184
|
+
violations.push({
|
|
185
|
+
kind: scoped.length > 0 ? 'not_in_source' : 'carrier_direct_unverified',
|
|
186
|
+
line: c.line,
|
|
187
|
+
detail: `承运级直飞断言「${c.carrier_only} 直飞」无 exact-date 证据(${c.origin ?? '?'}→${c.destination ?? '?'} ${c.date ?? '?'})` + (scoped.length > 0 && scoped.every((f)=>f.bookability === 'bookable_exact_date' && f.nonstop === false) ? '——该日可售仅有中转' : '——「可考虑/也许有」不得写入决策树')
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const r = flightClaimVerdict(facts, c);
|
|
193
|
+
if (r.verdict === 'traceable') {
|
|
194
|
+
if (lineTimesContradict(c.text, r.fact?.dep_local, r.fact?.arr_local)) {
|
|
195
|
+
violations.push({
|
|
196
|
+
kind: 'contradicted',
|
|
197
|
+
line: c.line,
|
|
198
|
+
detail: `${c.flight_no} 时刻与 exact-date 快照不符(快照 ${r.fact?.dep_local}→${r.fact?.arr_local} ${r.fact?.date})——渲染层不得改写工具返回的时刻`
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
traceable++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
violations.push({
|
|
206
|
+
kind: r.verdict,
|
|
207
|
+
line: c.line,
|
|
208
|
+
detail: `${c.flight_no}: ${r.reason}`
|
|
209
|
+
});
|
|
210
|
+
if (CHECK_MARK.test(c.text)) {
|
|
211
|
+
violations.push({
|
|
212
|
+
kind: 'unconditional_check',
|
|
213
|
+
line: c.line,
|
|
214
|
+
detail: `对未核验航班 ${c.flight_no} 使用无条件 ✓/✅——只有 bookable_exact_date 才允许确定性标记`
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const d of claims.direct_lines){
|
|
219
|
+
if (claims.flights.some((c)=>c.line === d.line)) continue;
|
|
220
|
+
if (!d.origin || !d.destination || !d.date) {
|
|
221
|
+
violations.push({
|
|
222
|
+
kind: 'route_unqueried',
|
|
223
|
+
line: d.line,
|
|
224
|
+
detail: `直飞断言缺 route/date 上下文,无法回溯——fail closed 按未核验处理`
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const scoped = latestFactsForRouteDate(facts, d.origin, d.destination, d.date);
|
|
229
|
+
if (scoped.length === 0) {
|
|
230
|
+
violations.push({
|
|
231
|
+
kind: 'route_unqueried',
|
|
232
|
+
line: d.line,
|
|
233
|
+
detail: `${d.origin}→${d.destination} ${d.date} 的直飞断言从未经 exact-date 源核验`
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const nonstopCount = scoped.filter((f)=>f.bookability === 'bookable_exact_date' && f.nonstop !== false).length;
|
|
238
|
+
if (nonstopCount === 0) {
|
|
239
|
+
violations.push({
|
|
240
|
+
kind: 'not_in_source',
|
|
241
|
+
line: d.line,
|
|
242
|
+
detail: `${d.origin}→${d.destination} ${d.date} exact-date 无任何在架直飞(仅中转或 0 条)——不得写直飞`
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const a of claims.airports){
|
|
247
|
+
const table = map.carrier_airport[a.city] ?? {};
|
|
248
|
+
for (const carrier of a.carriers){
|
|
249
|
+
const expected = table[carrier];
|
|
250
|
+
if (expected && expected !== a.airport) {
|
|
251
|
+
violations.push({
|
|
252
|
+
kind: 'airport_mapping_conflict',
|
|
253
|
+
line: a.line,
|
|
254
|
+
detail: `${carrier} 在${a.city}落 ${expected}(映射快照 as_of ${map.meta.as_of}),产物写 ${a.airport}——双机场不互换,航司不合并映射`
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const p of claims.policies){
|
|
260
|
+
if (!p.has_as_of) {
|
|
261
|
+
violations.push({
|
|
262
|
+
kind: 'policy_without_as_of',
|
|
263
|
+
line: p.line,
|
|
264
|
+
detail: `政策断言缺时间边界:动态政策只能表述为「截至 YYYY-MM-DD 的现行政策」并附复核日期`
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (CHECK_MARK.test(p.text) && !p.has_as_of) {
|
|
268
|
+
violations.push({
|
|
269
|
+
kind: 'unconditional_check',
|
|
270
|
+
line: p.line,
|
|
271
|
+
detail: `政策行使用无条件 ✓/✅`
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
markdown.split('\n').forEach((line, i)=>{
|
|
276
|
+
if (!line.includes('联程')) return;
|
|
277
|
+
const nos = [
|
|
278
|
+
...line.matchAll(FLIGHT_NO)
|
|
279
|
+
].map((m)=>m[1].toUpperCase());
|
|
280
|
+
const matched = facts.filter((f)=>f.kind !== 'policy' && nos.includes(f.flight_no.toUpperCase()));
|
|
281
|
+
const allProtected = nos.length > 0 && matched.length === nos.length && matched.every((f)=>f.protected_connection === true);
|
|
282
|
+
if (!allProtected) {
|
|
283
|
+
violations.push({
|
|
284
|
+
kind: 'self_transfer_called_through',
|
|
285
|
+
line: i + 1,
|
|
286
|
+
detail: '「联程」措辞无 protected_connection=true 证据——两段接得上不等于同票联程;分票必须标红自助转机(无行李直挂/误机无保护)'
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
if (opts?.itinerary) {
|
|
291
|
+
for (const v of itineraryInvariants(opts.itinerary)){
|
|
292
|
+
violations.push({
|
|
293
|
+
kind: v.kind,
|
|
294
|
+
line: 0,
|
|
295
|
+
detail: v.detail
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
schema: ARTIFACT_GATE_SCHEMA,
|
|
301
|
+
verdict: violations.length > 0 ? 'blocked' : 'pass',
|
|
302
|
+
claims_checked: claims.flights.length + claims.policies.length + claims.airports.length,
|
|
303
|
+
traceable,
|
|
304
|
+
violations,
|
|
305
|
+
presentation: violations.length > 0 ? 'verified_label_forbidden' : 'verified_itinerary_allowed'
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/src/artifact-gate.ts
|