From 7288114e33d48f0f0d8084f435f897f8b11ec2d0 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Tue, 5 May 2026 12:39:10 +0800
Subject: [PATCH] =?UTF-8?q?fix(game):=20=E5=89=A5=E7=A6=BB=E8=BF=90?=
=?UTF-8?q?=E8=A1=8C=E6=97=B6=E6=A8=A1=E5=9D=97=E5=AF=B9=E7=94=9F=E6=88=90?=
=?UTF-8?q?=E5=99=A8=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 GFramework.Game 对 SourceGenerators.Abstractions 的项目引用并移除未使用的枚举生成 attribute
- 新增 runtime-generator 边界校验脚本并接入 CI 与发布打包校验
- 更新 AGENTS、贡献文档与 ai-plan 跟踪,明确运行时模块禁止依赖生成器能力
---
.github/workflows/ci.yml | 3 +
.github/workflows/publish.yml | 3 +
AGENTS.md | 3 +
.../Config/YamlConfigSchemaPropertyType.cs | 3 -
.../Config/YamlConfigStringFormatKind.cs | 3 -
GFramework.Game/GFramework.Game.csproj | 1 -
ai-plan/public/README.md | 7 +
.../runtime-generator-boundary-tracking.md | 40 +++
.../runtime-generator-boundary-trace.md | 21 ++
docs/zh-CN/contributing.md | 8 +
.../validate-runtime-generator-boundaries.py | 286 ++++++++++++++++++
11 files changed, 371 insertions(+), 7 deletions(-)
create mode 100644 ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md
create mode 100644 ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md
create mode 100644 scripts/validate-runtime-generator-boundaries.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7484aa96..644e5cd4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,6 +35,9 @@ jobs:
- name: Validate license headers
run: python3 scripts/license-header.py --check
+ - name: Validate runtime-generator boundaries
+ run: python3 scripts/validate-runtime-generator-boundaries.py
+
# 缓存MegaLinter
- name: Cache MegaLinter
uses: actions/cache@v5
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index c9ba8930..500ac980 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -118,6 +118,9 @@ jobs:
diff -u expected-packages.txt actual-packages.txt
+ - name: Validate runtime-generator package boundaries
+ run: python3 scripts/validate-runtime-generator-boundaries.py --package-dir ./packages
+
- name: Show packages
run: ls -la ./packages || true
diff --git a/AGENTS.md b/AGENTS.md
index 55f4cc59..80e007a3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -212,6 +212,9 @@ All generated or modified code MUST include clear and meaningful comments where
- Private fields: `_camelCase`
- Keep abstractions projects free of implementation details and engine-specific dependencies.
- Preserve existing module boundaries. Do not introduce new cross-module dependencies without clear architectural need.
+- Framework runtime, abstractions, and meta-package projects MUST NOT reference `*.SourceGenerators*` projects or packages,
+ and MUST NOT use source-generator attributes such as `GenerateEnumExtensions` or `ContextAware`. Those capabilities are
+ reserved for consumer projects, generator projects, examples explicitly meant to demonstrate generator usage, and related tests.
### Formatting
diff --git a/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs b/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs
index af27219e..04bb0a43 100644
--- a/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs
+++ b/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs
@@ -1,14 +1,11 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
-using GFramework.Core.SourceGenerators.Abstractions.Enums;
-
namespace GFramework.Game.Config;
///
/// 表示当前运行时 schema 校验器支持的属性类型。
///
-[GenerateEnumExtensions]
internal enum YamlConfigSchemaPropertyType
{
///
diff --git a/GFramework.Game/Config/YamlConfigStringFormatKind.cs b/GFramework.Game/Config/YamlConfigStringFormatKind.cs
index 7233e88a..3b211933 100644
--- a/GFramework.Game/Config/YamlConfigStringFormatKind.cs
+++ b/GFramework.Game/Config/YamlConfigStringFormatKind.cs
@@ -1,14 +1,11 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
-using GFramework.Core.SourceGenerators.Abstractions.Enums;
-
namespace GFramework.Game.Config;
///
/// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。
///
-[GenerateEnumExtensions]
internal enum YamlConfigStringFormatKind
{
///
diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj
index 6d2e3a10..add4ad19 100644
--- a/GFramework.Game/GFramework.Game.csproj
+++ b/GFramework.Game/GFramework.Game.csproj
@@ -14,7 +14,6 @@
true
-
diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md
index 139061a5..b4fb1655 100644
--- a/ai-plan/public/README.md
+++ b/ai-plan/public/README.md
@@ -42,6 +42,10 @@ help the current worktree land on the right recovery documents without scanning
- Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow.
- Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md`
- Trace: `ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md`
+- `runtime-generator-boundary`
+ - Purpose: keep runtime and abstractions packages isolated from source-generator dependencies, packaging leaks, and attribute usage.
+ - Tracking: `ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md`
+ - Trace: `ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md`
## Worktree To Active Topic Map
@@ -68,6 +72,9 @@ help the current worktree land on the right recovery documents without scanning
- Branch: `fix/release-notes-pr-links`
- Worktree hint: `GFramework`
- Priority 1: `semantic-release-versioning`
+- Branch: `fix/runtime-generator-boundary`
+ - Worktree hint: `GFramework`
+ - Priority 1: `runtime-generator-boundary`
- Branch: `docs/sdk-update-documentation`
- Worktree hint: `GFramework-update-documentation`
- Priority 1: `documentation-full-coverage-governance`
diff --git a/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md b/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md
new file mode 100644
index 00000000..cf81840e
--- /dev/null
+++ b/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md
@@ -0,0 +1,40 @@
+# Runtime / Generator Boundary Tracking
+
+## Goal
+
+Keep runtime, abstractions, and meta-package modules free from source-generator project references, source-generator
+attributes, and leaked NuGet dependencies.
+
+## Current Recovery Point
+
+- Recovery point: RGB-RP-001
+- Phase: remove `GFramework.Game` generator coupling and add repository guardrails
+- Focus:
+ - delete `GFramework.Game`'s dependency on `GFramework.Core.SourceGenerators.Abstractions`
+ - remove unused `[GenerateEnumExtensions]` usage from `GFramework.Game`
+ - add static and packed-package validation so runtime packages cannot regress
+
+## Active Risks
+
+- A runtime package can still compile locally if it references a non-packable generator helper project, so regressions are
+ easy to miss without an explicit guard.
+- A leaked package dependency may only surface when a consumer restores from NuGet, not during normal repository builds.
+
+## Completed In This Stage
+
+- Confirmed `GFramework.Game` was the direct runtime offender and `GeWuYou.GFramework.Game` leaked
+ `GFramework.Core.SourceGenerators.Abstractions` into its nuspec dependency graph.
+- Confirmed the two `[GenerateEnumExtensions]` usages inside `GFramework.Game` do not need generated output and can be
+ removed outright.
+
+## Validation Target
+
+- `python3 scripts/validate-runtime-generator-boundaries.py`
+- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
+- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
+- `dotnet pack GFramework.sln -c Release -p:PackageVersion=`
+
+## Next Recommended Resume Step
+
+Run the new boundary validator plus minimal Release build and pack validation, then inspect `GeWuYou.GFramework.Game`
+and transitive runtime packages to confirm no `SourceGenerators` dependency remains.
diff --git a/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md b/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md
new file mode 100644
index 00000000..60ceb84c
--- /dev/null
+++ b/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md
@@ -0,0 +1,21 @@
+# Runtime / Generator Boundary Trace
+
+## 2026-05-05
+
+### RGB-RP-001 Runtime package boundary repair
+
+- Trigger:
+ - external consumers restoring `GeWuYou.GFramework.Game` failed because NuGet looked for
+ `GFramework.Core.SourceGenerators.Abstractions`
+ - repository inspection showed `GFramework.Game` had a direct project reference to a non-packable generator
+ abstractions project and used `[GenerateEnumExtensions]`
+- Decisions:
+ - treat the issue as a runtime/generator boundary violation, not as a missing publish target
+ - remove the runtime-side attribute usage instead of turning generator abstractions into public runtime packages
+ - add repository guardrails at both source-validation time and packed-package validation time
+- Expected implementation:
+ - `GFramework.Game` removes the generator abstractions project reference
+ - `GFramework.Game` removes the two unused enum generator attributes
+ - CI and publish workflows run a dedicated boundary validator script
+- Immediate next step:
+ - complete implementation and run Release build plus pack verification
diff --git a/docs/zh-CN/contributing.md b/docs/zh-CN/contributing.md
index ee760bcc..1628bed1 100644
--- a/docs/zh-CN/contributing.md
+++ b/docs/zh-CN/contributing.md
@@ -218,6 +218,14 @@ git push origin your-branch
### 命名规范
+### 代码生成器边界
+
+- 框架内部的运行时模块、抽象层模块和顶层元包不得依赖 `*.SourceGenerators*` 项目或包。
+- `GenerateEnumExtensions`、`ContextAware`、`GetModel`、`GetService` 等代码生成器 attribute 只允许出现在消费端项目、
+ 生成器项目本身、专门验证生成器行为的测试项目,或明确用于演示生成器接入的示例中。
+- 如果某个运行时模块为了编译而需要引入生成器 attribute,说明模块边界已经漂移;应优先移除该 attribute 使用,而不是把
+ generator abstractions 暴露成新的运行时依赖。
+
遵循 C# 标准命名约定:
- **类、接口、方法**:PascalCase
diff --git a/scripts/validate-runtime-generator-boundaries.py b/scripts/validate-runtime-generator-boundaries.py
new file mode 100644
index 00000000..75924d66
--- /dev/null
+++ b/scripts/validate-runtime-generator-boundaries.py
@@ -0,0 +1,286 @@
+#!/usr/bin/env python3
+# Copyright (c) 2025-2026 GeWuYou
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+import zipfile
+import xml.etree.ElementTree as ET
+from dataclasses import dataclass
+from pathlib import Path
+
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+
+RUNTIME_PROJECTS = (
+ "GFramework",
+ "GFramework.Core",
+ "GFramework.Core.Abstractions",
+ "GFramework.Cqrs",
+ "GFramework.Cqrs.Abstractions",
+ "GFramework.Game",
+ "GFramework.Game.Abstractions",
+ "GFramework.Godot",
+ "GFramework.Ecs.Arch",
+ "GFramework.Ecs.Arch.Abstractions",
+)
+
+FORBIDDEN_ATTRIBUTE_NAMES = (
+ "GenerateEnumExtensions",
+ "ContextAware",
+ "GetModel",
+ "GetModels",
+ "GetSystem",
+ "GetSystems",
+ "GetUtility",
+ "GetUtilities",
+ "GetService",
+ "GetServices",
+ "GetAll",
+ "Log",
+ "Priority",
+)
+
+FORBIDDEN_PROJECT_REFERENCE_PREFIX = "GFramework."
+FORBIDDEN_PACKAGE_REFERENCE_PREFIX = "GeWuYou.GFramework."
+PACKAGE_NAMESPACE = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"
+PACKAGE_NAMESPACE_2012 = "http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd"
+
+
+@dataclass(frozen=True)
+class Violation:
+ location: str
+ message: str
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Validate that runtime and abstractions modules do not depend on source-generator packages or attributes."
+ )
+ parser.add_argument(
+ "--package-dir",
+ type=Path,
+ help="Optional package directory. When supplied, validate packed runtime nuspec dependencies as well.",
+ )
+ return parser.parse_args()
+
+
+def get_project_file(project_name: str) -> Path:
+ if project_name == "GFramework":
+ return REPO_ROOT / "GFramework.csproj"
+
+ return REPO_ROOT / project_name / f"{project_name}.csproj"
+
+
+def get_source_root(project_name: str) -> Path | None:
+ if project_name == "GFramework":
+ return None
+
+ return REPO_ROOT / project_name
+
+
+def get_local_name(tag: str) -> str:
+ return tag.split("}", 1)[-1]
+
+
+def get_xml_text(element: ET.Element) -> str:
+ return (element.text or "").strip()
+
+
+def get_runtime_package_ids() -> set[str]:
+ package_ids: set[str] = set()
+ for project_name in RUNTIME_PROJECTS:
+ project_file = get_project_file(project_name)
+ tree = ET.parse(project_file)
+ root = tree.getroot()
+ package_id = None
+ assembly_name = None
+
+ for element in root.iter():
+ local_name = get_local_name(element.tag)
+ if local_name == "PackageId" and not package_id:
+ package_id = get_xml_text(element)
+ elif local_name == "AssemblyName" and not assembly_name:
+ assembly_name = get_xml_text(element)
+
+ resolved_package_id = package_id or f"GeWuYou.{assembly_name or project_name}"
+ package_ids.add(resolved_package_id)
+
+ return package_ids
+
+
+def validate_project_references() -> list[Violation]:
+ violations: list[Violation] = []
+
+ for project_name in RUNTIME_PROJECTS:
+ project_file = get_project_file(project_name)
+ tree = ET.parse(project_file)
+
+ for element in tree.getroot().iter():
+ local_name = get_local_name(element.tag)
+ if local_name not in {"ProjectReference", "PackageReference"}:
+ continue
+
+ include = element.attrib.get("Include", "").strip()
+ if local_name == "ProjectReference":
+ if FORBIDDEN_PROJECT_REFERENCE_PREFIX not in include or "SourceGenerators" not in include:
+ continue
+ else:
+ if not include.startswith(FORBIDDEN_PACKAGE_REFERENCE_PREFIX) or "SourceGenerators" not in include:
+ continue
+
+ violations.append(
+ Violation(
+ location=str(project_file.relative_to(REPO_ROOT)),
+ message=f"forbidden {local_name} -> {include}",
+ )
+ )
+
+ return violations
+
+
+def compile_attribute_patterns() -> dict[str, re.Pattern[str]]:
+ patterns: dict[str, re.Pattern[str]] = {}
+ for attribute_name in FORBIDDEN_ATTRIBUTE_NAMES:
+ patterns[attribute_name] = re.compile(
+ rf"\[(?P[^\]]*?(?:^|[\s,(])(?:global::)?(?:[A-Za-z_][A-Za-z0-9_]*\.)*{attribute_name}(?:Attribute)?(?=[\s,)\]])[^\]]*)\]",
+ re.MULTILINE,
+ )
+
+ return patterns
+
+
+def line_number_for_offset(text: str, offset: int) -> int:
+ return text.count("\n", 0, offset) + 1
+
+
+def validate_source_attributes() -> list[Violation]:
+ violations: list[Violation] = []
+ patterns = compile_attribute_patterns()
+
+ for project_name in RUNTIME_PROJECTS:
+ source_root = get_source_root(project_name)
+ if source_root is None or not source_root.is_dir():
+ continue
+
+ for file_path in source_root.rglob("*.cs"):
+ if any(part in {"bin", "obj"} for part in file_path.parts):
+ continue
+
+ text = file_path.read_text(encoding="utf-8-sig")
+ for attribute_name, pattern in patterns.items():
+ for match in pattern.finditer(text):
+ line_number = line_number_for_offset(text, match.start())
+ relative_path = file_path.relative_to(REPO_ROOT)
+ violations.append(
+ Violation(
+ location=f"{relative_path}:{line_number}",
+ message=f"forbidden source-generator attribute [{attribute_name}] in runtime module",
+ )
+ )
+
+ return violations
+
+
+def iter_dependency_ids(nuspec_root: ET.Element) -> list[str]:
+ dependency_ids: list[str] = []
+ for namespace in (PACKAGE_NAMESPACE, PACKAGE_NAMESPACE_2012):
+ dependency_ids.extend(
+ element.attrib["id"]
+ for element in nuspec_root.findall(f".//{{{namespace}}}dependency")
+ if "id" in element.attrib
+ )
+
+ if dependency_ids:
+ return dependency_ids
+
+ dependency_ids.extend(
+ element.attrib["id"]
+ for element in nuspec_root.findall(".//dependency")
+ if "id" in element.attrib
+ )
+ return dependency_ids
+
+
+def validate_packed_dependencies(package_dir: Path) -> list[Violation]:
+ violations: list[Violation] = []
+ runtime_package_ids = get_runtime_package_ids()
+
+ for package_path in sorted(package_dir.glob("*.nupkg")):
+ with zipfile.ZipFile(package_path) as archive:
+ nuspec_entries = [name for name in archive.namelist() if name.endswith(".nuspec")]
+ if not nuspec_entries:
+ violations.append(
+ Violation(
+ location=str(package_path.relative_to(REPO_ROOT if package_path.is_relative_to(REPO_ROOT) else package_dir.parent)),
+ message="missing nuspec entry",
+ )
+ )
+ continue
+
+ nuspec_root = ET.fromstring(archive.read(nuspec_entries[0]))
+
+ package_id_element = nuspec_root.find(f".//{{{PACKAGE_NAMESPACE}}}id")
+ if package_id_element is None:
+ package_id_element = nuspec_root.find(f".//{{{PACKAGE_NAMESPACE_2012}}}id")
+ if package_id_element is None:
+ package_id_element = nuspec_root.find(".//id")
+
+ package_id = get_xml_text(package_id_element) if package_id_element is not None else package_path.stem
+ if package_id not in runtime_package_ids:
+ continue
+
+ dependency_ids = iter_dependency_ids(nuspec_root)
+ for dependency_id in dependency_ids:
+ if not dependency_id.startswith(FORBIDDEN_PACKAGE_REFERENCE_PREFIX) and not dependency_id.startswith(
+ FORBIDDEN_PROJECT_REFERENCE_PREFIX
+ ):
+ continue
+
+ if "SourceGenerators" not in dependency_id:
+ continue
+
+ violations.append(
+ Violation(
+ location=str(package_path),
+ message=f"runtime package {package_id} depends on forbidden package {dependency_id}",
+ )
+ )
+
+ return violations
+
+
+def print_violations(violations: list[Violation]) -> None:
+ for violation in violations:
+ print(f"- {violation.location}: {violation.message}")
+
+
+def main() -> int:
+ args = parse_args()
+
+ violations: list[Violation] = []
+ violations.extend(validate_project_references())
+ violations.extend(validate_source_attributes())
+
+ if args.package_dir is not None:
+ package_dir = args.package_dir if args.package_dir.is_absolute() else REPO_ROOT / args.package_dir
+ if not package_dir.is_dir():
+ print(f"Package directory does not exist: {package_dir}", file=sys.stderr)
+ return 2
+
+ violations.extend(validate_packed_dependencies(package_dir))
+
+ if violations:
+ print("Runtime/source-generator boundary validation failed.")
+ print_violations(violations)
+ return 1
+
+ print("Runtime/source-generator boundary validation passed.")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())