Skip to content

Commit 99f0d39

Browse files
committed
Test the falsification condition, and report that it holds
The positioning document named the condition under which this project is more than a nicer way to write a gate: if a contract-driven gate and an equivalent if/else gate do not differ measurably on unsupported completion, the value collapses. This runs that test on `can_deliver` and the answer is that the condition holds. One thing is decidable before any measurement: `can_deliver`'s policy is an if/else chain over the answer probabilities, so the policy layer cannot be where a difference lives. The experiment therefore varies only frame construction and validation, and holds the thresholds character-for-character identical across arms. Four arms over six scenarios and six perturbations (36 runs each): contract 33 ok 0 false-accept 9 false-reject 0 refused contract-strict 30 0 6 6 ifelse-best 30 0 6 6 ifelse-naive 33 0 9 0 Cell by cell: `contract` is identical to `ifelse-naive` on all six perturbations, and `contract-strict` is identical to `ifelse-best`. They differ from each other only when a declared cell goes missing. The sharpest finding corrects the positioning document rather than confirming it: the reference runtime records a missing cell in `unfilled` and nothing consumes it, so the contract as shipped is indistinguishable from a hand-written gate that never considered drift. The refusal that does exist lives in the Codex adapter, and a best-practice hand-written gate reproduces it exactly. "Refusal at load time" is an adapter property, and the document was crediting the contract for it. Two more results worth as much as the headline: · A zero false-accept rate under drift is not safety. Empty evidence makes every claim unsupported, so the gate rejects everything — it has stopped working. That is why every table reports both axes, and why `ifelse-best` was written with a guard: a gate that rejects everything is perfect on false-accepts alone. · `history: []` is refused by nothing, including the adapter discipline. `unfilled` detects a key that was never supplied, not a supplied value that is empty — and that is the transcript reader's actual failure shape. Two perturbations (type drift) changed no verdict in any arm and are reported as failed manipulations rather than deleted; deleting them would record an experiment that did not happen as a conclusion that did. The oracle is hand-authored and the judge is a substring proxy. Both are stated as the weakest parts of the design rather than buried, since a hand-labelled oracle presented as automatic is what makes this kind of number look harder than it is. The semantic judge (hand-written LLM gate) needs a real backend and is the second arm. Run: npm run gate-compare. Pinned by 11 tests, including a mutation check that fails if the reference runtime ever starts refusing a missing cell.
1 parent 41fcdf9 commit 99f0d39

10 files changed

Lines changed: 1157 additions & 3 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ The files that carry the idea, in reading order:
522522
| [`docs/SKILL-DECISION-ADAPTER.md`](docs/SKILL-DECISION-ADAPTER.md) | how to adapt an existing runtime, step by step |
523523
| [`docs/ADAPTER-CODEX-SCOPE.md`](docs/ADAPTER-CODEX-SCOPE.md) | that skill applied to a real host we do not own — what is reachable, and what is not |
524524
| [`docs/POSITIONING-spec-vs-hook.md`](docs/POSITIONING-spec-vs-hook.md) | ★ a specification or a hook — what this is worth, and the condition each reading depends on |
525+
| [`docs/MEASUREMENT-gate-equivalence.md`](docs/MEASUREMENT-gate-equivalence.md) | ★ that condition, tested: a contract gate vs an equivalent `if`/`else` gate — the falsification holds |
525526
| [`adapters/codex/`](adapters/codex/README.md) | a working adapter for that host, built by following the skill |
526527

527528
Everything else is plumbing. The parts you are most likely to want to replace:

‎README.zh-CN.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ CWD_ROOT=./你的项目 PORT=7800 npm run serve
444444
| [`docs/SKILL-DECISION-ADAPTER.md`](docs/SKILL-DECISION-ADAPTER.md) | 逐步改造一个已有运行时 |
445445
| [`docs/ADAPTER-CODEX-SCOPE.md`](docs/ADAPTER-CODEX-SCOPE.md) | 把那份 Skill 用在一个我们不拥有的真实宿主上 —— 哪些接得到、哪些接不到 |
446446
| [`docs/POSITIONING-spec-vs-hook.md`](docs/POSITIONING-spec-vs-hook.md) | ★ 它是一份规范,还是一个 hook —— 以及两种读法各自依赖什么条件 |
447+
| [`docs/MEASUREMENT-gate-equivalence.md`](docs/MEASUREMENT-gate-equivalence.md) | ★ 把那个条件测了:契约门 vs 等价的 `if`/`else` 门 —— falsification 成立 |
447448
| [`adapters/codex/`](adapters/codex/README.md) | 照着 Skill 做出来的那个真实适配器 |
448449

449450
其余都是接线。最可能想换掉的那几块:

‎bench/gate-arms.ts‎

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* JevLoop · 完成闸门对照实验:四条臂(纯逻辑,无 IO、无 console)
3+
*
4+
* ══════════════════════════════════════════════════════════════
5+
* **这个文件是「机制」,不是「报告」。**
6+
* ══════════════════════════════════════════════════════════════
7+
*
8+
* 拆出来的理由有两个,第二个更要紧:
9+
*
10+
* ① `bench/gate-compare.ts` 是命令行入口,它在模块顶层 `await main()` ——
11+
* 于是**不能被 import**(一 import 就整轮跑起来)。测试要能直接调这些臂。
12+
* ② 结论要能被**钉住**。这一轮的读数(契约门在参考实现里的行为与「没写守卫的
13+
* 手写门」逐格相同)如果只是打印出来,下次谁改了 `frame.ts` 的处置方式,
14+
* 报告会**静静地变成假的**。钉成测试之后,它变的是断言,不是文档。
15+
*
16+
* ── 四条臂 ──────────────────────────────────────────────────────
17+
*
18+
* contract 契约门,照**参考实现真实的行为**:帧由 `frameArtifact` 编,
19+
* 缺格记进 `unfilled`,然后**没人读它**,策略照跑
20+
* contract-strict 同上 + `unfilled` 非空即拒绝(codex 适配器的纪律)
21+
* ifelse-best 手写门,带「宿主没给证据」的守卫 —— **公平的那个对手**
22+
* ifelse-naive 手写门,没写守卫
23+
*
24+
* ★ 策略层在四条臂里**逐字相同**(`unsupported >= 0.5 → revise`;
25+
* `deliverable >= 0.6 → deliver`)。这是刻意的:策略层可以机械地翻译成
26+
* if/else,所以任何「契约判得更准」的读数都只可能来自帧,不来自策略。
27+
* 如果这里让两臂的门限写得不一样,测出来的差异就没有意义了。
28+
*
29+
* @module JevLoop/gate-arms
30+
*/
31+
32+
import { buildDecisions } from '../src/decisions.ts'
33+
import { resolvePolicy } from '../src/policy.ts'
34+
import type { AgentCtx, Frame } from '../src/frame.ts'
35+
import type { AnswerSet } from '../src/vocab.ts'
36+
import type { GateScenario } from './gate-scenarios.ts'
37+
import type { ScenarioCtx } from './gate-drift.ts'
38+
39+
/** 一条臂一次运行给出的东西 */
40+
export interface RunResult {
41+
outcome: 'deliver' | 'revise' | 'refuse' | 'error'
42+
why: string
43+
/** 这条臂**说出**了哪些格子的异常。空数组 = 它一个字都没说 */
44+
noticed: string[]
45+
}
46+
47+
/** 相对 oracle 的记账。`refused` 单独计 —— 它不是「判错」,是「不判」 */
48+
export type VerdictKind = 'ok' | 'false-accept' | 'false-reject' | 'refused' | 'error'
49+
50+
/** 全部记账类别,报告的顺序 */
51+
export const KINDS: VerdictKind[] = ['ok', 'false-accept', 'false-reject', 'refused', 'error']
52+
53+
/**
54+
* 判据机 —— 读帧的确定性函数,四条臂共用。
55+
*
56+
* ★ **它只看得见传进来的那份 `evidence`。** 证据空了、成了数组、换成了别的东西,
57+
* 它就跟着给出不同的答案 —— 这正是「帧坏了,判定跟着坏」的真实路径。如果它
58+
* 绕开帧去读场景真值,这个实验就变成了在测脚本作者写对了没有。
59+
*
60+
* ★ `answer` / `evidence` 都收 `unknown` 并**对称地**做 `String()`。
61+
* 不对称会很隐蔽地替某一臂说话:一边崩、一边静默继续,读数就变成了
62+
* 「谁先把类型写对」,而不是「两种机制在漂移下各自做了什么」。
63+
*
64+
* ⚠️ 字面包含是**代理**,不是语义。真正的语义判据是第二臂(手写 LLM 门)。
65+
*/
66+
export function judge(
67+
claims: readonly { keyword: string }[],
68+
answer: unknown,
69+
evidence: unknown,
70+
): AnswerSet {
71+
// 正常消费者会把它当字符串读(投影声明的 `returns` 就是 `string`)——
72+
// 类型漂移**不会抛**,它只会静默地改变结果。
73+
const hay = typeof evidence === 'string' ? evidence : String(evidence)
74+
const said = typeof answer === 'string' ? answer : String(answer)
75+
const missing = claims.filter((c) => !hay.includes(c.keyword))
76+
// 回答为空 = 没有可核对的主张。★ 单独一类,不该算作「有证据支持」
77+
const emptyAnswer = said.trim() === ''
78+
const unsupported = missing.length > 0 || emptyAnswer ? 0.9 : 0.1
79+
const deliverable = unsupported > 0.5 ? 0.1 : 0.9
80+
return {
81+
unsupported: { type: 'noul', noul: unsupported },
82+
deliverable: { type: 'noul', noul: deliverable },
83+
}
84+
}
85+
86+
/** 帧里那些「宿主没喂 / 没投影出来 / 被截断」的格子 —— 契约**记下来了** */
87+
export function frameSignals(frame: Frame): string[] {
88+
const out: string[] = []
89+
for (const u of frame.unfilled) out.push(`unfilled:${u.key}`)
90+
for (const a of frame.absent) out.push(`absent:${a.key}`)
91+
for (const t of frame.truncated) out.push(`truncated:${t.key}`)
92+
return out
93+
}
94+
95+
/**
96+
* 契约门。
97+
*
98+
* @param strict `true` 时 `unfilled` 非空即拒绝 —— 那是 **codex 适配器**的纪律
99+
* (`adapters/codex/core.ts`),**不是**参考实现的行为。默认 `false` 才是
100+
* 「契约本身在漂移下会做什么」。
101+
*/
102+
export function contractArm(scenario: GateScenario, ctx: ScenarioCtx, strict: boolean): RunResult {
103+
const spec = buildDecisions().canDeliver
104+
const artifact = spec.frameArtifact
105+
if (!artifact) return { outcome: 'error', why: 'canDeliver 没有 frameArtifact', noticed: [] }
106+
107+
let frame: Frame
108+
try {
109+
frame = artifact(ctx as AgentCtx)
110+
} catch (err) {
111+
return { outcome: 'error', why: `编帧抛了:${(err as Error).message}`, noticed: [] }
112+
}
113+
114+
const noticed = frameSignals(frame)
115+
116+
if (strict && frame.unfilled.length > 0) {
117+
return {
118+
outcome: 'refuse',
119+
why: `缺 state 格:${frame.unfilled.map((u) => u.key).join(', ')}`,
120+
noticed,
121+
}
122+
}
123+
124+
const answers = judge(scenario.claims, frame.state.answer, frame.state.evidence)
125+
const out = resolvePolicy(spec.policy, answers)
126+
return { outcome: out.action as RunResult['outcome'], why: out.reason, noticed }
127+
}
128+
129+
/**
130+
* 手写门 —— **它自己**从原始 ctx 里取证据,不经过帧。
131+
*
132+
* ★ 这正是手写门与契约门真正的分界:契约的格子在 `DECISION.md` 里**声明**,
133+
* 手写门的格子在**代码里写死**。所以前者能自动多出一格,后者不会。
134+
*
135+
* @param guarded `true` = 最佳实践(含「宿主没给历史」的守卫),`false` = 没写守卫
136+
*/
137+
export function ifElseArm(scenario: GateScenario, ctx: ScenarioCtx, guarded: boolean): RunResult {
138+
const noticed: string[] = []
139+
140+
const history = ctx.history
141+
if (history === undefined) noticed.push('unfilled:history')
142+
else if (history.length === 0) noticed.push('empty:history')
143+
144+
if (guarded && ctx.draft === undefined) noticed.push('unfilled:draft')
145+
146+
if (guarded && noticed.some((n) => n.startsWith('unfilled:'))) {
147+
// 最佳实践:知道自己要证据,而宿主没给 ⇒ 拒绝,别猜
148+
return { outcome: 'refuse', why: `手写守卫:${noticed.join(', ')}`, noticed }
149+
}
150+
151+
const evidence = (history ?? []).map((s) => `${s.tool}(${s.input}) → ${s.result}`).join('\n')
152+
const answers = judge(scenario.claims, ctx.draft ?? '', evidence)
153+
const unsupported = (answers.unsupported as { noul: number }).noul
154+
const deliverable = (answers.deliverable as { noul: number }).noul
155+
// ── 策略:**逐字照抄** DECISION.md 的三条规则 ──
156+
const action = unsupported >= 0.5 ? 'revise' : deliverable >= 0.6 ? 'deliver' : 'revise'
157+
return { outcome: action, why: '手写 if/else(门限与契约一致)', noticed }
158+
}
159+
160+
/** 一条臂:id + 说明 + 怎么跑 */
161+
export interface Arm {
162+
id: string
163+
label: string
164+
run: (scenario: GateScenario, ctx: ScenarioCtx) => RunResult
165+
}
166+
167+
/** 四条臂,顺序即报告顺序 */
168+
export const ARMS: Arm[] = [
169+
{
170+
id: 'contract',
171+
label: '契约门(照参考实现的真实行为:记录缺格,但不处置)',
172+
run: (s, c) => contractArm(s, c, false),
173+
},
174+
{
175+
id: 'contract-strict',
176+
label: '契约门 + 缺格拒绝(codex 适配器的纪律)',
177+
run: (s, c) => contractArm(s, c, true),
178+
},
179+
{ id: 'ifelse-best', label: '手写门(最佳实践:带守卫)', run: (s, c) => ifElseArm(s, c, true) },
180+
{ id: 'ifelse-naive', label: '手写门(没写守卫)', run: (s, c) => ifElseArm(s, c, false) },
181+
]
182+
183+
/** 把一次运行结果相对 oracle 分类 */
184+
export function classify(result: RunResult, expected: GateScenario['expected']): VerdictKind {
185+
if (result.outcome === 'error') return 'error'
186+
if (result.outcome === 'refuse') return 'refused'
187+
if (result.outcome === expected) return 'ok'
188+
return expected === 'revise' ? 'false-accept' : 'false-reject'
189+
}
190+
191+
/**
192+
* 跑一条臂,**不让它把整轮带走**。
193+
*
194+
* ★ 一条臂抛异常也是一种结果。不接住的话,「谁先在类型上崩掉」会表现为
195+
* 「那个台子跑不出来」,读数就丢了一格。
196+
*/
197+
export function runArm(arm: Arm, scenario: GateScenario, ctx: ScenarioCtx): RunResult {
198+
try {
199+
return arm.run(scenario, ctx)
200+
} catch (err) {
201+
return { outcome: 'error', why: (err as Error).message, noticed: [] }
202+
}
203+
}

‎bench/gate-compare.ts‎

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* JevLoop · 完成闸门对照实验:报告
3+
*
4+
* node --experimental-strip-types bench/gate-compare.ts
5+
*
6+
* ══════════════════════════════════════════════════════════════
7+
* **它回答哪一句话,以及它不能回答哪一句话。**
8+
* ══════════════════════════════════════════════════════════════
9+
*
10+
* 定位文档(`docs/POSITIONING-spec-vs-hook.md`)把 falsification 条件写成了:
11+
*
12+
* 拿一个用 if/else 写成的完成闸门,和一个由决策契约驱动的同一道闸门比。
13+
* 如果 **unsupported completion** 上两者没有可测的差异,契约的价值就塌缩成
14+
* 「一种更好看的写法」。
15+
*
16+
* 这个台子就去测它。结论与两个**失败的扰动**写在
17+
* `docs/MEASUREMENT-gate-equivalence.md`;机制与四条臂在 `bench/gate-arms.ts`,
18+
* 用例与 oracle 在 `bench/gate-scenarios.ts`,漂移在 `bench/gate-drift.ts`。
19+
*
20+
* ── 它测不了什么 ────────────────────────────────────────────────
21+
*
22+
* 判据机是**字面包含**(见 `gate-scenarios.ts` 的 `Claim`),所以这里的
23+
* 「unsupported」是代理,不是语义。真正的语义判据是第二臂(手写 LLM 门),
24+
* 那需要真判定后端,不在这一轮。**不要把这份报告读成「闸门准不准」** ——
25+
* 它读的是「同一份判据下,两种机制在宿主漂移时各自做了什么」。
26+
*
27+
* @module JevLoop/gate-compare
28+
*/
29+
30+
import { buildDecisions } from '../src/decisions.ts'
31+
import type { AgentCtx } from '../src/frame.ts'
32+
import { SCENARIOS } from './gate-scenarios.ts'
33+
import { PERTURBATIONS, type ScenarioCtx } from './gate-drift.ts'
34+
import { ARMS, KINDS, classify, runArm, type VerdictKind } from './gate-arms.ts'
35+
36+
/** 扰动**实际**把 ctx 弄成了什么样 —— 与 `expect` 对账,不靠猜 */
37+
function observedShape(ctx: ScenarioCtx): string {
38+
const parts: string[] = []
39+
if (ctx.history === undefined) parts.push('history 键不存在')
40+
else if (ctx.history.length === 0) parts.push('history 为空数组')
41+
else if (ctx.history.some((s) => typeof s.result !== 'string')) parts.push('history.result 不是字符串')
42+
else parts.push('history 正常')
43+
if (typeof ctx.draft !== 'string') parts.push(`draft 是 ${typeof ctx.draft}`)
44+
return parts.join(' / ')
45+
}
46+
47+
/**
48+
* ★ 目击证词:契约门**实际编出来的那一格**是什么类型,以及它**说了什么**。
49+
*
50+
* 这一栏是为 P3 那两条加的:它们**没能改变任何一条臂的判定**,所以单看判定矩阵
51+
* 会读成「这个扰动什么都没发生」。事实上帧里那一格已经不是它声明的 `string`,
52+
* 而 `unfilled` / `absent` / `truncated` **没有一个会说这件事**。
53+
* 判定没变不代表帧没坏,这两件事必须分开报。
54+
*/
55+
function frameWitness(ctx: ScenarioCtx): string {
56+
const artifact = buildDecisions().canDeliver.frameArtifact
57+
if (!artifact) return '(没有 frameArtifact)'
58+
try {
59+
const f = artifact(ctx as AgentCtx)
60+
const t = (v: unknown) => (Array.isArray(v) ? `数组(${v.length})` : typeof v)
61+
return `answer=${t(f.state.answer)} evidence=${t(f.state.evidence)} unfilled=[${f.unfilled.map((u) => u.key).join(',')}] absent=[${f.absent.map((a) => a.key).join(',')}]`
62+
} catch (err) {
63+
return `编帧抛了:${(err as Error).message}`
64+
}
65+
}
66+
67+
async function main(): Promise<void> {
68+
const rows: {
69+
scenario: string
70+
perturbation: string
71+
arm: string
72+
observed: string
73+
kind: VerdictKind
74+
outcome: string
75+
}[] = []
76+
77+
for (const pert of PERTURBATIONS) {
78+
for (const scenario of SCENARIOS) {
79+
const ctx = pert.apply(scenario.ctx)
80+
for (const arm of ARMS) {
81+
const result = runArm(arm, scenario, ctx)
82+
rows.push({
83+
scenario: scenario.id,
84+
perturbation: pert.id,
85+
arm: arm.id,
86+
observed: observedShape(ctx),
87+
kind: classify(result, scenario.expected),
88+
outcome: result.outcome,
89+
})
90+
}
91+
}
92+
}
93+
94+
// ── 表一:每个扰动一张 场景 × 臂 的矩阵 ──
95+
//
96+
// ★ 为什么要有明细而不是只给汇总:汇总里的「假接受 0」很容易被读成「很安全」。
97+
// 明细才看得出**它是怎么得到 0 的** —— P1 下契约门那 0 其实是「证据一空,
98+
// 所有主张都不被支持,于是把每一份回答都打回」,那不是变准了,是闸门停摆了。
99+
// 只报汇总等于把「全部拒绝」装成满分。
100+
const SYM: Record<VerdictKind, string> = {
101+
ok: '✓',
102+
'false-accept': '假受',
103+
'false-reject': '假拒',
104+
refused: '拒绝',
105+
error: '崩',
106+
}
107+
console.log('\n每个扰动一张明细(行 = 场景,列 = 臂;✓ 表示与 oracle 一致)\n')
108+
for (const pert of PERTURBATIONS) {
109+
console.log(`── ${pert.id} ${pert.what}`)
110+
console.log(` ${'场景'.padEnd(26)}${ARMS.map((a) => a.id.padEnd(16)).join('')}oracle`)
111+
for (const s of SCENARIOS) {
112+
const cells = ARMS.map((a) => {
113+
const r = rows.find((x) => x.scenario === s.id && x.perturbation === pert.id && x.arm === a.id)
114+
return (r ? SYM[r.kind] : '?').padEnd(16)
115+
}).join('')
116+
console.log(` ${s.id.padEnd(26)}${cells}${s.expected}`)
117+
}
118+
const obs = [...new Set(rows.filter((r) => r.perturbation === pert.id).map((r) => r.observed))]
119+
console.log(` ↳ 实测形状:${obs.join(' / ')} (预期 ${pert.expect})`)
120+
console.log(` ↳ 帧实际内容(S1):${frameWitness(pert.apply(SCENARIOS[0].ctx))}\n`)
121+
}
122+
123+
// ── 表二:汇总 ──
124+
console.log('\n汇总(每臂 6 场景 × 6 扰动 = 36 次)\n')
125+
const header = `${'臂'.padEnd(16)}` + KINDS.map((k) => k.padEnd(14)).join('')
126+
console.log(header)
127+
console.log('─'.repeat(header.length + 6))
128+
for (const arm of ARMS) {
129+
const cell = rows.filter((r) => r.arm === arm.id)
130+
const counts = KINDS.map((k) => String(cell.filter((r) => r.kind === k).length).padEnd(14)).join('')
131+
console.log(`${arm.id.padEnd(16)}${counts}`)
132+
}
133+
134+
// ── 表三:逐格比较两臂是否给出同一结果 ──
135+
console.log('\n两臂在同一 (场景, 扰动) 上是否给出同一结果:\n')
136+
const pairs = [
137+
['contract', 'ifelse-naive'],
138+
['contract-strict', 'ifelse-best'],
139+
['contract', 'ifelse-best'],
140+
] as const
141+
for (const pert of PERTURBATIONS) {
142+
for (const [a, b] of pairs) {
143+
const diff = SCENARIOS.filter((s) => {
144+
const ra = rows.find((r) => r.scenario === s.id && r.perturbation === pert.id && r.arm === a)
145+
const rb = rows.find((r) => r.scenario === s.id && r.perturbation === pert.id && r.arm === b)
146+
return ra?.outcome !== rb?.outcome
147+
})
148+
console.log(
149+
` ${`${pert.id} · ${a} vs ${b}`.padEnd(50)} ${diff.length === 0 ? '逐格一致' : `不同:${diff.map((s) => s.id).join(', ')}`}`,
150+
)
151+
}
152+
}
153+
154+
// ── 表四:★ 反向对照必须无差异 ──
155+
console.log('\n★ 反向对照(P5)—— 只改帧声明**排除**的格子:\n')
156+
const p0 = rows.filter((r) => r.perturbation === 'P0-none')
157+
const p5 = rows.filter((r) => r.perturbation === 'P5-irrelevant-control')
158+
for (const arm of ARMS) {
159+
const a = p0.filter((r) => r.arm === arm.id).map((r) => `${r.scenario}:${r.outcome}`).join('|')
160+
const b = p5.filter((r) => r.arm === arm.id).map((r) => `${r.scenario}:${r.outcome}`).join('|')
161+
console.log(
162+
` ${arm.id.padEnd(16)} ${a === b ? '与基线一致 ✓' : '★ 与基线不同 ✗(对照失败,差异另有来源)'}`,
163+
)
164+
}
165+
console.log('')
166+
}
167+
168+
await main()

0 commit comments

Comments
 (0)