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 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 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,不阻断开发。 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).+$" } } } 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 ): diff --git a/tests/test_plugin_extension.py b/tests/test_plugin_extension.py index 2fd8753..d37c353 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,9 +67,36 @@ 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_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"]) + 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: + with self.subTest(value=repr(value)): + self.assertEqual(pattern.fullmatch(value) is not None, + _valid_project_path(value)) def test_bad_manifest_rejected(self): self.add('broken', {'id': 'broken', 'version':'1.0.0', 'domain':'code', 'standards':'not-a-list', 'detection':[]}) @@ -115,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'):