Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- "server/**"
- "proto/**"
- "scripts/**"
- "docs/**"
- "docker-compose.test.yml"
- ".github/workflows/e2e.yml"
pull_request:
Expand All @@ -19,6 +20,7 @@ on:
- "server/**"
- "proto/**"
- "scripts/**"
- "docs/**"
- "docker-compose.test.yml"
- ".github/workflows/e2e.yml"
workflow_dispatch:
Expand Down Expand Up @@ -116,6 +118,11 @@ jobs:
- name: Bring up Redis test service
run: docker compose -f docker-compose.test.yml up -d redis --wait

- name: C2S gate matrix contract
run: |
python3 scripts/check_c2s_gate_matrix.py
python3 -m unittest scripts/tests/check_c2s_gate_matrix_test.py

- name: Schema stage (build + check + test + generate)
working-directory: agent/packages/schema
run: |
Expand Down
364 changes: 364 additions & 0 deletions docs/plan-refactor-c2s-gate-v1.md

Large diffs are not rendered by default.

49 changes: 0 additions & 49 deletions docs/plans-skeleton/plan-refactor-c2s-gate-v1.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/plans-skeleton/plan-refactor-master-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@

1. 9 条轨道全部归档(各自 bot 场景常绿 + 吸收 plan 全部归档/验伪结案);
2. 三个 2 万行级 god file(inventory/mod.rs、client_request_handler.rs、persistence/mod.rs)不复存在,最大单文件 < 3000 行;
3. `qi_current` 裸写编译不过;client 无未登记的会话态 store;113 C2S 变体全部有显式 GateSpec/no_gate 声明;28 旁路 channel 收编或豁免登记;
3. `qi_current` 裸写编译不过;client 无未登记的会话态 store;届时现行 `ClientRequestV1` 全部变体均有显式 GateSpec/no_gate 声明(2026-08-03 P0 基线为 104,新增变体自动纳入);28 旁路 channel 收编或豁免登记;
4. bot 场景数从 ~30 增至 ≥80,CI e2e 是唯一主门禁且无已知假绿。
5. `flash-review` label 下 open issue 全部显式处置(fixed / dup / 验伪关闭 / 促升 skeleton,见 §10),无静默积压。

Expand Down
2 changes: 1 addition & 1 deletion docs/plans-skeleton/plan-refactor-wire-s2c-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
- ⬜ P1 emit builder + scope 落地:builder 上线,vfx/audio/env 三类先挂 scope(跨维 bleed 立灭);跨位面切换时 env/season 全量重发。
- ⬜ P2 client 桥接层收敛:枚举前缀剥离收敛到单点(含 forge-session 修复);`ServerDataRouter` 注册表整备(分域注册文件,不再单个 1547 行 switch 追加)。
- ⬜ P3 旁路归一批次:28 channel 逐批收编入 server_data envelope 或登记豁免(资源包/握手类可豁免);删除散装 receiver。
- ⬜ P4 契约 pin 全量化:双向 sample 对拍测试补齐(113 C2S + 144 S2C 每变体至少一条正反 sample,schema 改动连 sample 一起改);emit 迁移到 builder 的长尾批次。
- ⬜ P4 契约 pin 全量化:双向 sample 对拍测试补齐(届时现行 `ClientRequestV1` 全部变体,2026-08-03 基线 104;以及届时现行 `ServerDataType` 全部变体,2026-07-27 侦察基线 144;每变体至少一条正反 sample,schema 改动连 sample 一起改);emit 迁移到 builder 的长尾批次。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

python3 - <<'PY'
from pathlib import Path
import re

text = Path("agent/packages/schema/src/server-data.ts").read_text(encoding="utf-8")
block = text.split("export const ServerDataType = Type.Union([", 1)[1]
block = block.split("]);", 1)[0]
tags = re.findall(r'Type\.Literal\("([^"]+)"\)', block)

print(f"ServerDataType literal count: {len(tags)}")
if len(tags) != 144:
    raise SystemExit("ServerDataType count does not match the documented 144 baseline")
PY

Repository: Kizunad/Bong

Length of output: 248


🏁 Script executed:

#!/bin/sh
set -eu

echo "== plan =="
sed -n '1,80p' docs/plans-skeleton/plan-refacor-wire-s2c-v1.md 2>/dev/null || true

echo "== file match =="
fd -a 'plan-refactor-wire-s2c-v1\.md|server-data\.ts' . | sed 's#^\./##'

echo "== schema outline/search =="
wc -l agent/packages/schema/src/server-data.ts
python3 - <<'PY'
from pathlib import Path
text = Path("agent/packages/schema/src/server-data.ts").read_text(encoding="utf-8")
print("contains ServerDataType:", "export const ServerDataType = Type.Union([" in text)
print("line 190-305:")
for i,line in enumerate(text.splitlines(), 1):
    if 190 <= i <= 305:
        print(f"{i}: {line}")
PY

echo "== enumerate all Type.Literal values in server-data.ts =="
python3 - <<'PY'
import re
from pathlib import Path
text = Path("agent/packages/schema/src/server-data.ts").read_text(encoding="utf-8")
print("all exported/labeled datatypes:")
for i,line in enumerate(text.splitlines(), 1):
    if "export const " in line or "Type.Union" in line:
        print(f"{i}: {line}")
print("ServerDataType total literals:", len(re.findall(r'Type\.Literal\("([^"]+)"\)', text)))
PY

echo "== search for 144 and ServerDataType =="
rg -n "144|ServerDataType|ClientRequestV1|Client.*base|基线" docs/plans-skeleton docs agent packages 2>/dev/null | head -n 200

Repository: Kizunad/Bong

Length of output: 50368


🏁 Script executed:

#!/bin/sh
set -eu

echo "== plan exact =="
sed -n '1,70p' docs/plans-skeleton/plan-refactor-wire-s2c-v1.md

echo "== schema TypeScript exports from server-data.ts =="
python3 - <<'PY'
import re
from pathlib import Path
text = Path("agent/packages/schema/src/server-data.ts").read_text()
lines = text.splitlines()
for i,line in enumerate(lines, 1):
    if i < 20 or line.startswith(("export const ", "export type ") or "Type.Union" in line):
        if i <= 400:
            print(f"{i}: {line}")
PY

echo "== Rust ServerData enum search =="
fd -a 'server_data\.(rs|rs)$$' . | sed 's#^\./##'
rg -n "enum ServerData|ServerData(v1)?|ServerDataType|Type::" server agent 2>/dev/null | sed -n '1,240p'

echo "== agent package source files near ServerDataV1 =="
rg -n "ServerDataV1|ServerDataType" docs/plans-skeleton/plan-refactor-wire-s2c-v1.md agent/packages/schema src 2>/dev/null | sed -n '1,200p'

echo "== precise literal count in ServerDataType union block =="
python3 - <<'PY'
import re
from pathlib import Path
text = Path("agent/packages/schema/src/server-data.ts").read_text()
block = text.split("export const ServerDataType = Type.Union([",1)[1]
block = block.split("]);",1)[0]
vals = re.findall(r'Type\.Literal\("([^"]+)"\)', block)
print("count", len(vals))
PY

Repository: Kizunad/Bong

Length of output: 44261


修正 ServerDataType 基线或补全说明。

P4 的 ServerDataType 侦察基线写为 144,但 agent schema 的 ServerDataType literal 计数为 100。若这 144 包含旁路 channel 或其他服务端 Rust 变体,请标注来源;否则将基线改为当前 agent union 数量,避免 P4 按错误总量留空验证。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans-skeleton/plan-refactor-wire-s2c-v1.md` at line 25, 修正 P4 计划中
ServerDataType 基线的来源或数值:核对 agent schema 的 ServerDataType union 计数;若 144 包含旁路
channel 或其他服务端 Rust 变体,明确标注其来源和范围,否则将基线改为当前 agent union 的实际数量,避免按错误总量规划验证。

- ⬜ P5 bot 验收 + 吸收 plan 批量归档。

## 吸收清单(短名省略 plan-bughunt- 前缀与 -v1 后缀)
Expand Down
278 changes: 278 additions & 0 deletions scripts/check_c2s_gate_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
from __future__ import annotations

import re
import sys
from collections import Counter
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
ENUM_PATH = ROOT / "server/src/schema/client_request.rs"
PLAN_PATH = ROOT / "docs/plan-refactor-c2s-gate-v1.md"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ENUM_DECL_RE = re.compile(r"(?m)^[ \t]*pub\s+enum\s+ClientRequestV1\s*\{")
SERDE_ATTR_RE = re.compile(r"(?m)^[ \t]*#\[serde\(([^]]*)\)\][ \t]*$")
MATRIX_SECTION_RE = re.compile(r"^## P0 104 变体门禁矩阵\s*$")
MATRIX_HEADER_RE = re.compile(r"^\|\s*#\s*\|\s*`ClientRequestV1`\s*\|")
MATRIX_RE = re.compile(r"^\|\s*(\d+)\s*\|\s*`([^`]+)`\s*\|")
VARIANT_DECL_RE = re.compile(r"^([A-Z][A-Za-z0-9_]*)\s*(.*)$")


def _blank_non_newlines(masked: list[str], start: int, end: int) -> None:
for index in range(start, end):
if masked[index] != "\n":
masked[index] = " "


def _mask_rust_non_code(source: str) -> str:
masked = list(source)
index = 0
while index < len(source):
if source.startswith("//", index):
end = source.find("\n", index)
end = len(source) if end == -1 else end
_blank_non_newlines(masked, index, end)
index = end
continue
if source.startswith("/*", index):
start = index
depth = 1
index += 2
while index < len(source) and depth:
if source.startswith("/*", index):
depth += 1
index += 2
elif source.startswith("*/", index):
depth -= 1
index += 2
else:
index += 1
_blank_non_newlines(masked, start, index)
continue
raw_prefix = 2 if source.startswith("br", index) else 1 if source.startswith("r", index) else 0
if raw_prefix:
quote_index = index + raw_prefix
while quote_index < len(source) and source[quote_index] == "#":
quote_index += 1
if quote_index < len(source) and source[quote_index] == '"':
delimiter = '"' + source[index + raw_prefix : quote_index]
end = source.find(delimiter, quote_index + 1)
end = len(source) if end == -1 else end + len(delimiter)
_blank_non_newlines(masked, index, end)
index = end
continue
if source[index] == '"':
end = index + 1
while end < len(source):
if source[end] == "\\":
end += 2
elif source[end] == '"':
end += 1
break
else:
end += 1
_blank_non_newlines(masked, index, end)
index = end
continue
index += 1
return "".join(masked)


def _without_line_comment(line: str) -> str:
return line.split("//", 1)[0]


def _leading_attributes(code: str) -> tuple[list[str], str]:
attributes: list[str] = []
while code.startswith("#["):
bracket_depth = 0
in_string = False
escaped = False
end = None
for index, char in enumerate(code):
if in_string:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
in_string = False
continue
if char == '"':
in_string = True
elif char == "[":
bracket_depth += 1
elif char == "]":
bracket_depth -= 1
if bracket_depth == 0:
end = index + 1
break
if end is None:
raise RuntimeError(f"unterminated ClientRequestV1 attribute: {code!r}")
attributes.append(code[:end])
code = code[end:].lstrip()
if code.startswith("#"):
raise RuntimeError(f"unsupported ClientRequestV1 attribute syntax: {code!r}")
return attributes, code


def parse_enum_variants(source: str) -> list[str]:
masked = _mask_rust_non_code(source)
declaration = ENUM_DECL_RE.search(masked)
if not declaration:
raise RuntimeError(f"cannot parse ClientRequestV1 from {ENUM_PATH}")
serde = None
for match in reversed(list(SERDE_ATTR_RE.finditer(masked, 0, declaration.start()))):
if masked[match.end() : declaration.start()].strip():
break
serde = source[match.start(1) : match.end(1)]
break
if serde is None or not re.search(r'\btag\s*=\s*"type"', serde):
raise RuntimeError("ClientRequestV1 must use serde tag = \"type\"")
if not re.search(r'\brename_all\s*=\s*"snake_case"', serde):
raise RuntimeError("ClientRequestV1 must use serde rename_all = \"snake_case\"")

lines = source.splitlines()
masked_lines = masked.splitlines()
variants: list[str] = []
inside = False
depth = 0
tuple_depth = 0
pending_attributes: list[str] = []

for line, masked_line in zip(lines, masked_lines):
if not inside:
if masked_line.strip() == "pub enum ClientRequestV1 {":
inside = True
depth = 1
continue

code = _without_line_comment(line).strip()
structural_code = masked_line.strip()
if depth == 1 and tuple_depth:
tuple_depth += structural_code.count("(") - structural_code.count(")")
if tuple_depth < 0:
raise RuntimeError(f"unbalanced tuple variant syntax: {line!r}")
continue
if depth == 1 and structural_code == "}":
depth = 0
break
if depth == 1 and structural_code:
if code == ",":
continue
attributes, code = _leading_attributes(code)
pending_attributes.extend(attributes)
if not code:
continue
if any("serde" in attribute and "rename" in attribute for attribute in pending_attributes):
raise RuntimeError("ClientRequestV1 variant-level serde rename is unsupported")
pending_attributes.clear()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
match = VARIANT_DECL_RE.match(code)
if not match:
raise RuntimeError(f"unsupported ClientRequestV1 syntax: {line!r}")
suffix = match.group(2).strip()
if not suffix or suffix[0] not in "{(,":
raise RuntimeError(f"unsupported ClientRequestV1 variant syntax: {line!r}")
variants.append(match.group(1))
if suffix[0] == "(":
tuple_depth = suffix.count("(") - suffix.count(")")
if tuple_depth < 0:
raise RuntimeError(f"unbalanced tuple variant syntax: {line!r}")

depth += structural_code.count("{") - structural_code.count("}")
if depth == 0:
break

if not inside or depth != 0 or not variants:
raise RuntimeError(f"cannot parse ClientRequestV1 from {ENUM_PATH}")
return variants


def enum_variants() -> list[str]:
return parse_enum_variants(ENUM_PATH.read_text(encoding="utf-8"))


def parse_matrix_variants(source: str) -> tuple[list[int], list[str]]:
rows: list[tuple[int, str]] = []
in_matrix = False
header_seen = False
for line in source.splitlines():
if not in_matrix:
if MATRIX_SECTION_RE.match(line):
in_matrix = True
continue
if line.startswith("## "):
break
if not header_seen:
if MATRIX_HEADER_RE.match(line):
header_seen = True
continue
if match := MATRIX_RE.match(line):
rows.append((int(match.group(1)), match.group(2)))
if not rows:
raise RuntimeError(f"cannot parse C2S matrix from {PLAN_PATH}")
return [number for number, _ in rows], [variant for _, variant in rows]


def matrix_variants() -> tuple[list[int], list[str]]:
return parse_matrix_variants(PLAN_PATH.read_text(encoding="utf-8"))


def duplicates(values: list[str]) -> list[str]:
return sorted(value for value, count in Counter(values).items() if count > 1)


def first_order_mismatch(left: list[str], right: list[str]) -> tuple[int, str, str] | None:
for index in range(max(len(left), len(right))):
left_value = left[index] if index < len(left) else "<missing>"
right_value = right[index] if index < len(right) else "<missing>"
if left_value != right_value:
return index, left_value, right_value
return None


def main() -> int:
errors: list[str] = []
try:
enum = enum_variants()
numbers, matrix = matrix_variants()
except RuntimeError as error:
print(f"C2S gate matrix check failed:\n- {error}", file=sys.stderr)
return 1

expected_numbers = list(range(1, len(matrix) + 1))
if numbers != expected_numbers:
errors.append(f"matrix numbering is not contiguous: {numbers}")

for label, values in (("enum", enum), ("matrix", matrix)):
duplicate_values = duplicates(values)
if duplicate_values:
errors.append(f"duplicate {label} variants: {duplicate_values}")

enum_matrix_missing = [variant for variant in enum if variant not in matrix]
matrix_enum_extra = [variant for variant in matrix if variant not in enum]
if enum_matrix_missing:
errors.append(f"missing matrix variants: {enum_matrix_missing}")
if matrix_enum_extra:
errors.append(f"extra matrix variants: {matrix_enum_extra}")
if set(matrix) != set(enum):
errors.append("matrix and Rust enum variant sets differ")

mismatch = first_order_mismatch(enum, matrix)
if mismatch:
errors.append(
"first enum/matrix order mismatch at row "
f"{mismatch[0] + 1}: enum={mismatch[1]} matrix={mismatch[2]}"
)

if errors:
print("C2S gate matrix check failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1

print(f"C2S gate matrix matches all {len(enum)} Rust ClientRequestV1 variants")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading