Skip to content
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions docs/plugin-authoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 中文

本指南面向希望为 FrameForge 增加游戏引擎或专业领域支持的社区贡献者。**优先提交声明式 Manifest,不要先写代码。** 当前外部插件是只读元数据:FrameForge 不导入、不执行插件脚本,也不会因发现引擎而自动启用插件。
本指南面向希望为 FrameForge 增加游戏引擎或专业领域支持的社区贡献者。**优先提交声明式 Manifest,不要先写代码。** 当前外部插件是只读元数据:FrameForge 不导入、不执行插件脚本。发现只负责推荐;专业指导由项目 Profile 显式选择启用,且不依赖自动发现标记。

### 1. 先确定贡献类型

Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions docs/plugin-extension-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,不阻断开发。
14 changes: 9 additions & 5 deletions docs/plugin-manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -192,7 +196,7 @@
"projectPath": {
"type": "string",
"minLength": 1,
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\\\.\\\\.(?:/|$))(?!.*\\\\\\\\).+$"
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\)(?!.*\\x00).+$"
}
}
}
2 changes: 1 addition & 1 deletion src/frameforge/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
39 changes: 31 additions & 8 deletions tests/test_plugin_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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':[]})
Expand Down Expand Up @@ -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'):
Expand Down
Loading