From 15eeb553ddbb593ded96b8a7d090bf0e56e021f9 Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:45:44 +0800 Subject: [PATCH 1/9] fix: reject blank professional guidance fields --- src/frameforge/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frameforge/core.py b/src/frameforge/core.py index 7dd350d..395cdbb 100644 --- a/src/frameforge/core.py +++ b/src/frameforge/core.py @@ -65,7 +65,7 @@ def _validate_manifest(obj, source): guidance = obj.get('professional_guidance', []) if not isinstance(guidance, list) or not all( isinstance(rule, dict) and all(isinstance(rule.get(k), str) - and 0 < len(rule[k]) <= 512 for k in ('id', 'topic', 'signal', 'recommendation')) + and 0 < len(rule[k]) <= 512 and bool(rule[k].strip()) for k in ('id', 'topic', 'signal', 'recommendation')) and rule.get('source_suffix', '.cs' if obj['id']=='unity' else '.lua') in ('.cs','.lua','.asmdef') for rule in guidance ): From 5e7f98832a901f512e51130686ce25742af8dd10 Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:45:49 +0800 Subject: [PATCH 2/9] fix: align manifest path schema with runtime validation --- docs/plugin-manifest.schema.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/plugin-manifest.schema.json b/docs/plugin-manifest.schema.json index 1753fdf..7f56fb9 100644 --- a/docs/plugin-manifest.schema.json +++ b/docs/plugin-manifest.schema.json @@ -108,22 +108,26 @@ "id": { "type": "string", "minLength": 1, - "maxLength": 512 + "maxLength": 512, + "pattern": "\\S" }, "topic": { "type": "string", "minLength": 1, - "maxLength": 512 + "maxLength": 512, + "pattern": "\\S" }, "signal": { "type": "string", "minLength": 1, - "maxLength": 512 + "maxLength": 512, + "pattern": "\\S" }, "recommendation": { "type": "string", "minLength": 1, - "maxLength": 512 + "maxLength": 512, + "pattern": "\\S" }, "source_suffix": { "enum": [ @@ -192,7 +196,7 @@ "projectPath": { "type": "string", "minLength": 1, - "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\\\.\\\\.(?:/|$))(?!.*\\\\\\\\).+$" + "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\)(?!.*\\x00).+$" } } } From 67325f91953dfc93bef8ef9dd078f288e0c05180 Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:45:53 +0800 Subject: [PATCH 3/9] test: verify manifest schema and runtime validation parity --- tests/test_plugin_extension.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/test_plugin_extension.py b/tests/test_plugin_extension.py index 2fd8753..c841499 100644 --- a/tests/test_plugin_extension.py +++ b/tests/test_plugin_extension.py @@ -2,7 +2,7 @@ import tempfile import unittest from pathlib import Path -from frameforge.core import manifests, inspect +from frameforge.core import manifests, inspect, _valid_project_path class PluginExtensionTests(unittest.TestCase): @@ -67,7 +67,33 @@ def test_manifest_sources_validate_required_fields_and_date(self): with self.assertRaisesRegex(ValueError, 'Invalid plugin source date'): manifests(self.plugins) - def test_manifest_professional_guidance_accepts_schema_nonempty_whitespace(self): + def test_manifest_professional_guidance_rejects_whitespace_only_fields(self): + for field in ("id", "topic", "signal", "recommendation"): + with self.subTest(field=field): + guidance = {"id": "guidance", "topic": "topic", "signal": "signal", + "recommendation": "recommendation"} + guidance[field] = " " + self.add("guidance-space-" + field, { + "id": "guidance-space-" + field, "version": "1.0.0", + "domain": "code", "standards": ["architecture"], + "detection": [["custom.project"]], + "professional_guidance": [guidance], + }) + with self.assertRaisesRegex(ValueError, "Invalid professional guidance"): + manifests(self.plugins) + + def test_schema_project_path_pattern_matches_runtime_path_boundary(self): + import re + schema_path = Path(__file__).resolve().parents[1] / "docs/plugin-manifest.schema.json" + schema = json.loads(schema_path.read_text(encoding="utf-8")) + pattern = re.compile(schema["$defs"]["projectPath"]["pattern"]) + values = ["../outside", "a/../b", "src/../file.cs", r"a\\b", + "bad\\x00path", "/etc/passwd", "C:/outside", "src/file.cs"] + for value in values: + with self.subTest(value=repr(value)): + self.assertEqual(pattern.fullmatch(value) is not None, + _valid_project_path(value)) + self.add('guidance-space', {'id': 'guidance-space', 'version': '1.0.0', 'domain': 'code', 'standards': ['architecture'], 'detection': [['custom.project']], 'professional_guidance': [{'id': ' ', 'topic': ' ', 'signal': ' ', 'recommendation': ' '}]}) self.assertIn('guidance-space', manifests(self.plugins)) From 8e8427f9878f324aa80f3280e46c40ec82897654 Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:45:56 +0800 Subject: [PATCH 4/9] docs: clarify explicit plugin activation in English README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3949cd4..b5b57f7 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ CLI 保持 `frameforge /path/to/project`;如需 AI 客户端直接调用,可 ## English -FrameForge is a standalone, read-only project framework discovery and standards advisory tool. Projects own their canonical standards. Discovery may return path-level advisory previews but never activates professional checks, and findings never create a development gate. See the [Core responsibility and plugin boundary](docs/core-responsibility-and-plugin-boundary-v1.md). +FrameForge is a standalone, read-only project framework discovery and standards advisory tool. Projects own their canonical standards. Discovery suggests plugins but does not activate them; explicit Profile selection activates professional guidance independently of discovery markers. Findings never create a development gate. See the [Core responsibility and plugin boundary](docs/core-responsibility-and-plugin-boundary-v1.md). ### Quick start From d4070776a3683e04d1ed121ba7ec1b2bc40746cc Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:45:59 +0800 Subject: [PATCH 5/9] docs: clarify discovery and activation in authoring guide --- docs/plugin-authoring-guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugin-authoring-guide.md b/docs/plugin-authoring-guide.md index 586100d..7d7e9bd 100644 --- a/docs/plugin-authoring-guide.md +++ b/docs/plugin-authoring-guide.md @@ -2,7 +2,7 @@ ## 中文 -本指南面向希望为 FrameForge 增加游戏引擎或专业领域支持的社区贡献者。**优先提交声明式 Manifest,不要先写代码。** 当前外部插件是只读元数据:FrameForge 不导入、不执行插件脚本,也不会因发现引擎而自动启用插件。 +本指南面向希望为 FrameForge 增加游戏引擎或专业领域支持的社区贡献者。**优先提交声明式 Manifest,不要先写代码。** 当前外部插件是只读元数据:FrameForge 不导入、不执行插件脚本。发现只负责推荐;专业指导由项目 Profile 显式选择启用,且不依赖自动发现标记。 ### 1. 先确定贡献类型 @@ -77,7 +77,7 @@ PR 应包含:插件 ID/版本、引擎版本范围、官方/社区来源、Man ## English -This guide is for contributors adding game-engine or specialist guidance. **Start with a declarative manifest, not code.** External plugins are read-only metadata: FrameForge does not import or execute plugin scripts, and engine discovery never activates a plugin. +This guide is for contributors adding game-engine or specialist guidance. **Start with a declarative manifest, not code.** External plugins are read-only metadata: FrameForge does not import or execute plugin scripts. Discovery only suggests plugins; professional guidance runs only after explicit Profile selection, independently of discovery markers. ### 1. Choose the contribution type From 74493076ab1519d89ca515ea52007d5de488982d Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:46:02 +0800 Subject: [PATCH 6/9] docs: align extension contract with explicit activation --- docs/plugin-extension-v1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugin-extension-v1.md b/docs/plugin-extension-v1.md index cdb92e4..df9ad47 100644 --- a/docs/plugin-extension-v1.md +++ b/docs/plugin-extension-v1.md @@ -15,6 +15,6 @@ Manifest example: {"id":"custom-engine","version":"1.0.0","domain":"code","standards":["architecture"],"detection":[["custom.project"]]} ``` -Discovery suggests a plugin but does not activate it; a project must still explicitly select an ID and version in its Framework Profile. Missing standards produce advisory warnings, not development gates. +Discovery suggests a plugin but does not activate it. A project must explicitly select an ID and version in its Framework Profile; that selection activates professional guidance independently of discovery markers. Missing standards produce advisory warnings, not development gates. -中文:可以通过 `--plugin-dir` 明确指定外部插件描述文件目录。默认不扫描外部目录,也不执行插件代码。插件 ID 不允许覆盖内置项;发现技术栈只作推荐,启用仍由项目 Profile 决定。规范缺失为 Warning,不阻断开发。 +中文:可以通过 `--plugin-dir` 明确指定外部插件描述文件目录。默认不扫描外部目录,也不执行插件代码。插件 ID 不允许覆盖内置项;发现技术栈只作推荐,专业指导由项目 Profile 显式选择启用,且不依赖自动发现标记。规范缺失为 Warning,不阻断开发。 From 385737149031f3010711cda888404572253cb91c Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:46:32 +0800 Subject: [PATCH 7/9] test: remove obsolete whitespace acceptance fixture --- tests/test_plugin_extension.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_plugin_extension.py b/tests/test_plugin_extension.py index c841499..1349247 100644 --- a/tests/test_plugin_extension.py +++ b/tests/test_plugin_extension.py @@ -87,6 +87,10 @@ def test_schema_project_path_pattern_matches_runtime_path_boundary(self): schema_path = Path(__file__).resolve().parents[1] / "docs/plugin-manifest.schema.json" schema = json.loads(schema_path.read_text(encoding="utf-8")) pattern = re.compile(schema["$defs"]["projectPath"]["pattern"]) + guidance_fields = schema["properties"]["professional_guidance"]["items"]["properties"] + for field in ("id", "topic", "signal", "recommendation"): + self.assertIsNotNone(re.search(guidance_fields[field]["pattern"], "valid value")) + self.assertIsNone(re.search(guidance_fields[field]["pattern"], " ")) values = ["../outside", "a/../b", "src/../file.cs", r"a\\b", "bad\\x00path", "/etc/passwd", "C:/outside", "src/file.cs"] for value in values: @@ -94,9 +98,6 @@ def test_schema_project_path_pattern_matches_runtime_path_boundary(self): self.assertEqual(pattern.fullmatch(value) is not None, _valid_project_path(value)) - self.add('guidance-space', {'id': 'guidance-space', 'version': '1.0.0', 'domain': 'code', 'standards': ['architecture'], 'detection': [['custom.project']], 'professional_guidance': [{'id': ' ', 'topic': ' ', 'signal': ' ', 'recommendation': ' '}]}) - self.assertIn('guidance-space', manifests(self.plugins)) - def test_bad_manifest_rejected(self): self.add('broken', {'id': 'broken', 'version':'1.0.0', 'domain':'code', 'standards':'not-a-list', 'detection':[]}) with self.assertRaisesRegex(ValueError, 'Invalid plugin manifest'): From 03039ae6534c6e0854a9ed9152e48c27ce358488 Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:46:45 +0800 Subject: [PATCH 8/9] test: cover single backslash and NUL manifest paths --- tests/test_plugin_extension.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_plugin_extension.py b/tests/test_plugin_extension.py index 1349247..206bb45 100644 --- a/tests/test_plugin_extension.py +++ b/tests/test_plugin_extension.py @@ -91,8 +91,8 @@ def test_schema_project_path_pattern_matches_runtime_path_boundary(self): for field in ("id", "topic", "signal", "recommendation"): self.assertIsNotNone(re.search(guidance_fields[field]["pattern"], "valid value")) self.assertIsNone(re.search(guidance_fields[field]["pattern"], " ")) - values = ["../outside", "a/../b", "src/../file.cs", r"a\\b", - "bad\\x00path", "/etc/passwd", "C:/outside", "src/file.cs"] + values = ["../outside", "a/../b", "src/../file.cs", r"a\b", + "bad\x00path", "/etc/passwd", "C:/outside", "src/file.cs"] for value in values: with self.subTest(value=repr(value)): self.assertEqual(pattern.fullmatch(value) is not None, From deefcf0d9b0350d425fb39c4b144c9df0cc50930 Mon Sep 17 00:00:00 2001 From: bewaterhere-coder Date: Sun, 1 Nov 2026 10:48:05 +0800 Subject: [PATCH 9/9] test: remove duplicate legacy whitespace acceptance test --- tests/test_plugin_extension.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_plugin_extension.py b/tests/test_plugin_extension.py index 206bb45..d37c353 100644 --- a/tests/test_plugin_extension.py +++ b/tests/test_plugin_extension.py @@ -142,10 +142,6 @@ def test_manifest_sources_validate_required_fields_and_date(self): with self.assertRaisesRegex(ValueError, 'Invalid plugin source date'): manifests(self.plugins) - def test_manifest_professional_guidance_accepts_schema_nonempty_whitespace(self): - self.add('guidance-space', {'id': 'guidance-space', 'version': '1.0.0', 'domain': 'code', 'standards': ['architecture'], 'detection': [['custom.project']], 'professional_guidance': [{'id': ' ', 'topic': ' ', 'signal': ' ', 'recommendation': ' '}]}) - self.assertIn('guidance-space', manifests(self.plugins)) - def test_bad_manifest_rejected(self): self.add('broken', {'id': 'broken', 'version':'1.0.0', 'domain':'code', 'standards':'not-a-list', 'detection':[]}) with self.assertRaisesRegex(ValueError, 'Invalid plugin manifest'):