Skip to content

P0: OTel 事件信封 + 裁判双向盲评与锚定 rubric - #17

Merged
LeoLin990405 merged 2 commits into
mainfrom
feature/p0-otel-events-blind-judge
Jul 22, 2026
Merged

LeoLin990405 merged 2 commits into
mainfrom
feature/p0-otel-events-blind-judge

Conversation

@LeoLin990405

Copy link
Copy Markdown
Owner

P0: OTel 事件信封 + 裁判双向盲评与锚定 rubric

落实迭代路线图的 P0 两项(调研结论:事件流可观测化 + 裁判双向盲评)。

改动

1. 事件流 OTel 风格信封(schema v2)

每个事件在保留 legacy 字段(matchId/seq/ts/type)的基础上,纯增量追加:

  • event_id(UUID)、schema_version="2.0"trace_id(=matchId)
  • span_id / parent_span_idmatch_start 为根 span,其余事件默认挂根,可显式覆盖
  • actor(judge / skill-learner / system / regime)、kindtool_call|judge_score|skill_commit|...)、payload_hash(sha256[:16])
  • model / model_version / prompt_hash / tokens / cost 可选透传

同步更新 schemas/match-event.schema.json(新字段全部 optional,向后兼容)与前端类型。

2. 裁判双向盲评 + 锚定 rubric

  • 顺序交换双评:每场对决跑两遍,第二遍交换呈现顺序(A/B→B/A),分数跨 pass 取均值;CIVAGENT_JUDGE_SWAP=0 可关闭
  • 锚定 rubric:沿用现有三维(legality/feasibility/resilience),改为四点量表 + 每维 4 级锚点,要求结构化 JSON 输出;解析失败回退原 markdown 解析(向后兼容)
  • 审计:每个 pass 发一条 judge_score 事件,带 provider/modelprompt_hashswappedorderparent_span_id
  • manifest judge 段新增 swap/passes/rubric/events## Verdict 段保留(前端依赖)
  • judge_chain 回退(codex → opencode-reviewer → cn-glm)与 judge.mjs 零改动

测试

  • 基线 79 pass → 94 pass / 0 fail(新增 15 例)
  • npm run lint:syntax 通过

待决策(不阻塞合并)

  1. schema_version 取值:"2.0" vs "2.0.0"
  2. 分数归一 /10 vs 直接暴露 1-4 原始分(后者需改前端)
  3. rubric 锚点英文措辞需人工过一遍(JUDGE_RUBRIC_PROMPT
  4. 锦标赛级审计事件写在 matches/<tournamentId>/events.jsonl,可能被前端当作对局列出(有 tournament:true 标记可过滤)
  5. 目前裁判仍可见政体名(仅隐 backend);如需连政体身份也匿名另加映射层
  6. N>2 时 swap 为"整体反转",非两两全排列

Every event now carries, in addition to the legacy fields (matchId, seq,
ts, type — unchanged), an additive OTel-style envelope:

- event_id (UUID via crypto.randomUUID)
- schema_version ("2.0")
- trace_id (= match id, spans the whole match)
- span_id / parent_span_id (match_start is the trace root; other events
  default to its children unless an explicit parent is passed)
- actor (regime id / judge / skill-learner / system)
- kind (fine-grained: llm_call|tool_call|judge_score|skill_propose|
  skill_commit|turn|match_start|match_end; coexists with legacy type)
- payload_hash (sha256[:16] of the type-specific payload)
- optional pass-through: model, model_version, prompt_hash, tokens, cost

schemas/match-event.schema.json documents all new fields as optional and
updates examples; frontend MatchEvent type gains the same optional fields
(no consumer changes needed — parsing is additive-safe).
- Anchored rubric: the judge prompt now asks for scores on the three
  existing dimensions (legality, feasibility, resilience) using an
  anchored 4-point scale (1-4 with per-level anchor descriptions) and
  requires a structured JSON answer. A markdown-table fallback parser
  keeps legacy judge outputs working.
- Order swap: each matchup is judged twice (default on,
  CIVAGENT_JUDGE_SWAP=0 disables) — original order and reversed — and
  per-regime scores are averaged across passes to cancel presentation
  bias. For 2-civ duels this is exactly A/B → B/A.
- Audit trail: a tournament-level event stream (trace_id = tournament
  id) records one judge_score event per pass with provider/model,
  rubric prompt_hash (sha256[:16]), swapped flag, and presentation
  order, as child spans of a judge span under the trace root.
- Manifest gains judge.swap / judge.passes / judge.rubric / judge.events
  and per-regime rubric dims alongside the existing {regime, score}
  entries (scores normalized to /10 for leaderboard continuity).
- judge_chain fallback (codex → opencode-reviewer → cn-glm) unchanged.

Tests: +test/events-envelope.test.mjs (envelope presence, UUID/span
formats, trace topology), +test/judge-swap.test.mjs (swap calls judge
twice with reversed order, audit flags, mean aggregation, markdown
fallback, failure path), integration test extended with envelope and
judge-swap audit assertions.
LeoLin990405 added a commit that referenced this pull request Jul 22, 2026
The swap-judge rubric (PR #17) normalizes 1-4 dimension means to /10, so
the smallest score grain is ~0.83; with a 0.5 threshold almost nothing
would ever count as a tie. 0.8 aligns the tie band with the actual
measurement resolution. --tie remains available to override. Tests
assert the new default and the decisive side of the boundary.
@LeoLin990405
LeoLin990405 merged commit 09fa4b5 into main Jul 22, 2026
1 check passed
LeoLin990405 added a commit that referenced this pull request Jul 22, 2026
* feat(stats): cross-tournament Bradley-Terry ranking + bootstrap CI (P2)

Academic-grade evaluation pipeline over tournament manifests
(judge.scores = [{regime, score}]; extra per-score fields tolerated):

- engine/v5/stats.mjs (zero-dep):
  - collectManifests: scan a tournaments dir for */manifest.json
  - extractComparisons: per-tournament pairwise win records; |Δ| below
    the tie threshold (default 0.5, strict <, configurable) counts as a
    half-win tie
  - fitBT: Bradley-Terry abilities via MM iteration, geometric-mean
    normalized; a phantom-opponent prior (gamma=1, one fictitious game,
    half won) keeps abilities finite under perfect win/loss separation
  - analyze: bootstrap over tournaments (resample with replacement,
    mulberry32 seeded for reproducibility) → 95% ability CIs, rank
    intervals, median rank; pairwise significance from bootstrap log-
    ability differences (CI containing 0 = not separable) plus
    head-to-head records; explicit 样本不足 warning below 5 tournaments
  - formatTable human-readable ranking table
- CLI: 'civagent stats [--dir D] [--boot N] [--tie X] [--json] [dirs...]'
  (default dir ~/.civagent/tournaments; positional tournament dirs or
  manifest paths also accepted)
- test/stats.test.mjs (14 cases): BT ordering recovery on synthetic
  strong/mid/weak data, tie thresholds and boundary, equal abilities for
  tied regimes, significant vs alternating-rival pairs, small-sample
  warning, empty-input handling, PRNG determinism, CLI smoke incl.
  bin/civagent wiring.

* feat(stats): default tie threshold 0.5 → 0.8

The swap-judge rubric (PR #17) normalizes 1-4 dimension means to /10, so
the smallest score grain is ~0.83; with a 0.5 threshold almost nothing
would ever count as a tie. 0.8 aligns the tie band with the actual
measurement resolution. --tie remains available to override. Tests
assert the new default and the decisive side of the boundary.
@LeoLin990405
LeoLin990405 deleted the feature/p0-otel-events-blind-judge branch July 22, 2026 10:59
LeoLin990405 added a commit that referenced this pull request Jul 23, 2026
试批数据暴露的三个数据卫生问题:

1. DEFAULT_TIE_THRESHOLD 0.8 → 0.4:双评取均值后实际分数粒度 ~0.4,
   0.8 阈值下 tieRate 70.6%,BT 区分度崩塌。注释更新理由。
2. swap 时代过滤:#17 后 manifest 含 judge.swap 字段,量表与旧数据
   不同。analyze() 默认只统计 judge.swap === true 的锦标赛,跳过数
   通过 skippedLegacy + 警告 'skipped N legacy tournaments' 报告;
   CLI 新增 --include-legacy 恢复旧行为。splitSwapEra() 导出。
3. regime id 别名:内置 ALIASES = {global/athenian: global/athens},
   extractComparisons 收集时归一;~/.civagent/aliases.json 用户扩展
   合并(损坏文件容错)。hillclimb.mjs 的 scores/civs/skill 事件
   三处 regime 名同样归一(复用 loadAliases/normalizeRegime)。

测试:阈值边界(0.3 tie / 0.5 decisive @ 默认 0.4)、splitSwapEra
三类 manifest、analyze 跳过数与 includeLegacy、内置别名合并与
未知 id 不受影响、用户 aliases.json 扩展/覆盖/损坏容错、别名合并
后 BT 单一条目 games 累计。hillclimb 测试不受影响(夹具已带 swap)。

真实数据验证:13 场 swap 时代、5 场 legacy 正确跳过并警告,
athenian 不再单独出现。
LeoLin990405 pushed a commit that referenced this pull request Aug 6, 2026
- wanikua#37: Replace static AUTH_TOKEN with dynamic getAuthToken() to fix 401 after login
- #29: Change silijian agent id from 'main' to 'silijian' to avoid conflicts
- #19: Remove deprecated fields (runTimeoutSeconds, maxConcurrent), fix api field, add troubleshooting
- #20: Add --with-gui / --no-gui flag for optional Dashboard installation
- #31: Add comments recommending openclaw CLI over direct config editing
- #26: Add docs/windows-wsl.md for WSL2 setup guide
- #25/#17: Add hanlinyuan (翰林院) agent as optional configuration
LeoLin990405 pushed a commit that referenced this pull request Aug 6, 2026
LeoLin990405 added a commit that referenced this pull request Aug 6, 2026
* feat(events): add OTel-style envelope to match event stream (schema v2)

Every event now carries, in addition to the legacy fields (matchId, seq,
ts, type — unchanged), an additive OTel-style envelope:

- event_id (UUID via crypto.randomUUID)
- schema_version ("2.0")
- trace_id (= match id, spans the whole match)
- span_id / parent_span_id (match_start is the trace root; other events
  default to its children unless an explicit parent is passed)
- actor (regime id / judge / skill-learner / system)
- kind (fine-grained: llm_call|tool_call|judge_score|skill_propose|
  skill_commit|turn|match_start|match_end; coexists with legacy type)
- payload_hash (sha256[:16] of the type-specific payload)
- optional pass-through: model, model_version, prompt_hash, tokens, cost

schemas/match-event.schema.json documents all new fields as optional and
updates examples; frontend MatchEvent type gains the same optional fields
(no consumer changes needed — parsing is additive-safe).

* feat(judge): blind double evaluation with order swap + anchored rubric

- Anchored rubric: the judge prompt now asks for scores on the three
  existing dimensions (legality, feasibility, resilience) using an
  anchored 4-point scale (1-4 with per-level anchor descriptions) and
  requires a structured JSON answer. A markdown-table fallback parser
  keeps legacy judge outputs working.
- Order swap: each matchup is judged twice (default on,
  CIVAGENT_JUDGE_SWAP=0 disables) — original order and reversed — and
  per-regime scores are averaged across passes to cancel presentation
  bias. For 2-civ duels this is exactly A/B → B/A.
- Audit trail: a tournament-level event stream (trace_id = tournament
  id) records one judge_score event per pass with provider/model,
  rubric prompt_hash (sha256[:16]), swapped flag, and presentation
  order, as child spans of a judge span under the trace root.
- Manifest gains judge.swap / judge.passes / judge.rubric / judge.events
  and per-regime rubric dims alongside the existing {regime, score}
  entries (scores normalized to /10 for leaderboard continuity).
- judge_chain fallback (codex → opencode-reviewer → cn-glm) unchanged.

Tests: +test/events-envelope.test.mjs (envelope presence, UUID/span
formats, trace topology), +test/judge-swap.test.mjs (swap calls judge
twice with reversed order, audit flags, mean aggregation, markdown
fallback, failure path), integration test extended with envelope and
judge-swap audit assertions.
LeoLin990405 added a commit that referenced this pull request Aug 6, 2026
* feat(stats): cross-tournament Bradley-Terry ranking + bootstrap CI (P2)

Academic-grade evaluation pipeline over tournament manifests
(judge.scores = [{regime, score}]; extra per-score fields tolerated):

- engine/v5/stats.mjs (zero-dep):
  - collectManifests: scan a tournaments dir for */manifest.json
  - extractComparisons: per-tournament pairwise win records; |Δ| below
    the tie threshold (default 0.5, strict <, configurable) counts as a
    half-win tie
  - fitBT: Bradley-Terry abilities via MM iteration, geometric-mean
    normalized; a phantom-opponent prior (gamma=1, one fictitious game,
    half won) keeps abilities finite under perfect win/loss separation
  - analyze: bootstrap over tournaments (resample with replacement,
    mulberry32 seeded for reproducibility) → 95% ability CIs, rank
    intervals, median rank; pairwise significance from bootstrap log-
    ability differences (CI containing 0 = not separable) plus
    head-to-head records; explicit 样本不足 warning below 5 tournaments
  - formatTable human-readable ranking table
- CLI: 'civagent stats [--dir D] [--boot N] [--tie X] [--json] [dirs...]'
  (default dir ~/.civagent/tournaments; positional tournament dirs or
  manifest paths also accepted)
- test/stats.test.mjs (14 cases): BT ordering recovery on synthetic
  strong/mid/weak data, tie thresholds and boundary, equal abilities for
  tied regimes, significant vs alternating-rival pairs, small-sample
  warning, empty-input handling, PRNG determinism, CLI smoke incl.
  bin/civagent wiring.

* feat(stats): default tie threshold 0.5 → 0.8

The swap-judge rubric (PR #17) normalizes 1-4 dimension means to /10, so
the smallest score grain is ~0.83; with a 0.5 threshold almost nothing
would ever count as a tie. 0.8 aligns the tie band with the actual
measurement resolution. --tie remains available to override. Tests
assert the new default and the decisive side of the boundary.
LeoLin990405 added a commit that referenced this pull request Aug 6, 2026
试批数据暴露的三个数据卫生问题:

1. DEFAULT_TIE_THRESHOLD 0.8 → 0.4:双评取均值后实际分数粒度 ~0.4,
   0.8 阈值下 tieRate 70.6%,BT 区分度崩塌。注释更新理由。
2. swap 时代过滤:#17 后 manifest 含 judge.swap 字段,量表与旧数据
   不同。analyze() 默认只统计 judge.swap === true 的锦标赛,跳过数
   通过 skippedLegacy + 警告 'skipped N legacy tournaments' 报告;
   CLI 新增 --include-legacy 恢复旧行为。splitSwapEra() 导出。
3. regime id 别名:内置 ALIASES = {global/athenian: global/athens},
   extractComparisons 收集时归一;~/.civagent/aliases.json 用户扩展
   合并(损坏文件容错)。hillclimb.mjs 的 scores/civs/skill 事件
   三处 regime 名同样归一(复用 loadAliases/normalizeRegime)。

测试:阈值边界(0.3 tie / 0.5 decisive @ 默认 0.4)、splitSwapEra
三类 manifest、analyze 跳过数与 includeLegacy、内置别名合并与
未知 id 不受影响、用户 aliases.json 扩展/覆盖/损坏容错、别名合并
后 BT 单一条目 games 累计。hillclimb 测试不受影响(夹具已带 swap)。

真实数据验证:13 场 swap 时代、5 场 legacy 正确跳过并警告,
athenian 不再单独出现。
LeoLin990405 added a commit that referenced this pull request Aug 7, 2026
试批数据暴露的三个数据卫生问题:

1. DEFAULT_TIE_THRESHOLD 0.8 → 0.4:双评取均值后实际分数粒度 ~0.4,
   0.8 阈值下 tieRate 70.6%,BT 区分度崩塌。注释更新理由。
2. swap 时代过滤:#17 后 manifest 含 judge.swap 字段,量表与旧数据
   不同。analyze() 默认只统计 judge.swap === true 的锦标赛,跳过数
   通过 skippedLegacy + 警告 'skipped N legacy tournaments' 报告;
   CLI 新增 --include-legacy 恢复旧行为。splitSwapEra() 导出。
3. regime id 别名:内置 ALIASES = {global/athenian: global/athens},
   extractComparisons 收集时归一;~/.civagent/aliases.json 用户扩展
   合并(损坏文件容错)。hillclimb.mjs 的 scores/civs/skill 事件
   三处 regime 名同样归一(复用 loadAliases/normalizeRegime)。

测试:阈值边界(0.3 tie / 0.5 decisive @ 默认 0.4)、splitSwapEra
三类 manifest、analyze 跳过数与 includeLegacy、内置别名合并与
未知 id 不受影响、用户 aliases.json 扩展/覆盖/损坏容错、别名合并
后 BT 单一条目 games 累计。hillclimb 测试不受影响(夹具已带 swap)。

真实数据验证:13 场 swap 时代、5 场 legacy 正确跳过并警告,
athenian 不再单独出现。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant