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..a0f8fb72
--- /dev/null
+++ b/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md
@@ -0,0 +1,56 @@
+# 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.
+- Verified current PR review findings locally: the validator regex still missed standalone attributes, while the
+ `docs/zh-CN/contributing.md` generator-boundary text should be removed instead of repositioned because it is
+ maintainer-facing governance rather than reader-facing contribution guidance.
+- Added a Python regression test for standalone, parameterized, fully qualified, and multi-attribute declarations so
+ future validator edits cannot silently reintroduce the false negative.
+- Added comment-line filtering for the validator after the first regex fix started matching XML documentation examples
+ such as `/// [ContextAware]`, which would otherwise create false CI failures for reader-facing code comments.
+
+## Validation Target
+
+- `python3 scripts/validate-runtime-generator-boundaries.py`
+- `python3 scripts/test_validate_runtime_generator_boundaries.py`
+- `python3 scripts/license-header.py --check`
+- `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=`
+
+## Latest Validation Result
+
+- `python3 scripts/test_validate_runtime_generator_boundaries.py` passed on 2026-05-05.
+- `python3 scripts/validate-runtime-generator-boundaries.py` passed on 2026-05-05.
+- `python3 scripts/license-header.py --check` passed on 2026-05-05.
+- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` passed on 2026-05-05 with 0 warnings and 0 errors.
+
+## Next Recommended Resume Step
+
+Run the boundary validator, the new Python regression tests, and the minimal Release build/pack validation; then push
+the follow-up commit so the open PR review threads can be resolved against fresh CI.
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..ef1fa31f
--- /dev/null
+++ b/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md
@@ -0,0 +1,36 @@
+# 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
+- PR review follow-up:
+ - verified CodeRabbit and Greptile findings against local source before acting on them
+ - accepted the validator regex finding because the original pattern missed standalone
+ `[GenerateEnumExtensions]` declarations in runtime code
+ - added comment-line filtering after the first regex repair surfaced false positives from XML documentation examples
+ such as `/// [ContextAware]`
+ - rejected the documentation reposition suggestion as stated and removed the
+ `代码生成器边界` block from `docs/zh-CN/contributing.md` because it documents internal governance rather than
+ reader-facing contributor guidance
+ - added a Python regression test covering standalone, parameterized, fully qualified, and multi-attribute matches
+- Validation milestone:
+ - `python3 scripts/test_validate_runtime_generator_boundaries.py` passed
+ - `python3 scripts/validate-runtime-generator-boundaries.py` passed
+ - `python3 scripts/license-header.py --check` passed
+ - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` passed with 0 warnings and 0 errors
+- Immediate next step:
+ - push the PR follow-up commit and resolve the remaining review threads
diff --git a/scripts/test_validate_runtime_generator_boundaries.py b/scripts/test_validate_runtime_generator_boundaries.py
new file mode 100644
index 00000000..41899213
--- /dev/null
+++ b/scripts/test_validate_runtime_generator_boundaries.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+# Copyright (c) 2025-2026 GeWuYou
+# SPDX-License-Identifier: Apache-2.0
+
+"""Regression tests for runtime/source-generator boundary validation."""
+
+from __future__ import annotations
+
+import importlib.util
+import sys
+import unittest
+from pathlib import Path
+
+
+MODULE_PATH = Path(__file__).resolve().parent / "validate-runtime-generator-boundaries.py"
+MODULE_SPEC = importlib.util.spec_from_file_location("validate_runtime_generator_boundaries", MODULE_PATH)
+if MODULE_SPEC is None or MODULE_SPEC.loader is None:
+ raise RuntimeError(f"Unable to load module spec from {MODULE_PATH}")
+
+validate_runtime_generator_boundaries = importlib.util.module_from_spec(MODULE_SPEC)
+sys.modules[MODULE_SPEC.name] = validate_runtime_generator_boundaries
+MODULE_SPEC.loader.exec_module(validate_runtime_generator_boundaries)
+
+
+class ValidateRuntimeGeneratorBoundariesTests(unittest.TestCase):
+ """Covers attribute matching edge cases that previously caused false negatives."""
+
+ def setUp(self) -> None:
+ self.patterns = validate_runtime_generator_boundaries.compile_attribute_patterns()
+
+ def test_matches_standalone_attribute(self) -> None:
+ pattern = self.patterns["GenerateEnumExtensions"]
+
+ self.assertIsNotNone(pattern.search("[GenerateEnumExtensions]"))
+
+ def test_matches_parameterized_attribute(self) -> None:
+ pattern = self.patterns["GenerateEnumExtensions"]
+
+ self.assertIsNotNone(pattern.search("[GenerateEnumExtensions(typeof(string))]"))
+
+ def test_matches_non_leading_attribute_in_attribute_list(self) -> None:
+ pattern = self.patterns["GenerateEnumExtensions"]
+
+ self.assertIsNotNone(pattern.search("[Serializable, GenerateEnumExtensions]"))
+
+ def test_matches_fully_qualified_attribute(self) -> None:
+ pattern = self.patterns["Priority"]
+
+ self.assertIsNotNone(
+ pattern.search("[global::GFramework.Core.SourceGenerators.Abstractions.Bases.PriorityAttribute(10)]")
+ )
+
+ def test_ignores_xml_doc_example_attribute(self) -> None:
+ text = "/// [ContextAware]\npublic interface IController;\n"
+ pattern = self.patterns["ContextAware"]
+ match = pattern.search(text)
+
+ self.assertIsNotNone(match)
+ self.assertTrue(validate_runtime_generator_boundaries.is_comment_attribute_match(text, match.start()))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/validate-runtime-generator-boundaries.py b/scripts/validate-runtime-generator-boundaries.py
new file mode 100644
index 00000000..f27e237e
--- /dev/null
+++ b/scripts/validate-runtime-generator-boundaries.py
@@ -0,0 +1,296 @@
+#!/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", # GFramework.Core.SourceGenerators.Abstractions.Logging.LogAttribute
+ "Priority", # GFramework.Core.SourceGenerators.Abstractions.Bases.PriorityAttribute
+)
+
+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:
+ escaped_attribute_name = re.escape(attribute_name)
+ patterns[attribute_name] = re.compile(
+ rf"\[[^\]]*(?:(?<=\[)|(?<=[\s,(]))(?:global::)?(?:[A-Za-z_][A-Za-z0-9_]*\.)*{escaped_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 is_comment_attribute_match(text: str, offset: int) -> bool:
+ line_start = text.rfind("\n", 0, offset) + 1
+ line_prefix = text[line_start:offset].lstrip()
+ return line_prefix.startswith("///") or line_prefix.startswith("//") or line_prefix.startswith("/*") or line_prefix.startswith("*")
+
+
+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):
+ if is_comment_attribute_match(text, match.start()):
+ continue
+
+ 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())