Add GSP project manifest and design layout
This commit is contained in:
16
README.md
16
README.md
@@ -10,7 +10,7 @@ GSP 是一个通用游戏规格协议与配套工具链,用于在人类、AI
|
|||||||
GSP/
|
GSP/
|
||||||
language/ GSP 语言定义与 schema
|
language/ GSP 语言定义与 schema
|
||||||
toolkit/ GSP Toolkit Go CLI 源码
|
toolkit/ GSP Toolkit Go CLI 源码
|
||||||
examples/ 示例 GSP 文件
|
examples/ 示例 GSP 工程
|
||||||
```
|
```
|
||||||
|
|
||||||
生成产物默认放在:
|
生成产物默认放在:
|
||||||
@@ -70,4 +70,18 @@ bin/gsp.exe
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `language/README.md` | GSP 语言说明。 |
|
| `language/README.md` | GSP 语言说明。 |
|
||||||
| `language/gsp.schema.json` | GSP 核心字段 schema。 |
|
| `language/gsp.schema.json` | GSP 核心字段 schema。 |
|
||||||
|
| `language/gsp.manifest.schema.json` | GSP 工程 manifest schema。 |
|
||||||
| `toolkit/README.md` | GSP Toolkit 命令与实现说明。 |
|
| `toolkit/README.md` | GSP Toolkit 命令与实现说明。 |
|
||||||
|
|
||||||
|
## GSP 工程结构
|
||||||
|
|
||||||
|
GSP 工程默认使用以下结构:
|
||||||
|
|
||||||
|
```text
|
||||||
|
project/
|
||||||
|
gsp.manifest
|
||||||
|
design/
|
||||||
|
*.gsp
|
||||||
|
```
|
||||||
|
|
||||||
|
Toolkit 默认从工程根目录的 `design/` 目录扫描 `.gsp` 文件。`gsp.manifest` 可声明语言版本、Toolkit 版本、入口 GSP、扫描范围、阶段规则和 type 列表。
|
||||||
|
|||||||
28
examples/lottery/README.md
Normal file
28
examples/lottery/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Lottery GSP Example
|
||||||
|
|
||||||
|
这是一个 GSP 示例工程。
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
lottery/
|
||||||
|
gsp.manifest
|
||||||
|
design/
|
||||||
|
*.gsp
|
||||||
|
```
|
||||||
|
|
||||||
|
## 入口
|
||||||
|
|
||||||
|
```text
|
||||||
|
page.lottery.main
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生成
|
||||||
|
|
||||||
|
从仓库根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\bin\gsp.exe validate --root examples\lottery
|
||||||
|
.\bin\gsp.exe flatten page.lottery.main --root examples\lottery --depth -1
|
||||||
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format mermaid
|
||||||
|
```
|
||||||
39
examples/lottery/gsp.manifest
Normal file
39
examples/lottery/gsp.manifest
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
gspVersion: 0.1
|
||||||
|
toolkitVersion: 0.1
|
||||||
|
project: lottery-example
|
||||||
|
|
||||||
|
entry:
|
||||||
|
- page.lottery.main
|
||||||
|
|
||||||
|
scan:
|
||||||
|
- design
|
||||||
|
|
||||||
|
stageRules:
|
||||||
|
design:
|
||||||
|
minResolution: L0
|
||||||
|
integrate:
|
||||||
|
minResolution: L2
|
||||||
|
implement:
|
||||||
|
minResolution: L3
|
||||||
|
bind:
|
||||||
|
minResolution: L4
|
||||||
|
release:
|
||||||
|
minResolution: L5
|
||||||
|
|
||||||
|
types:
|
||||||
|
- note
|
||||||
|
- concept
|
||||||
|
- style
|
||||||
|
- system
|
||||||
|
- feature
|
||||||
|
- mechanic
|
||||||
|
- flow
|
||||||
|
- page
|
||||||
|
- ui
|
||||||
|
- interaction
|
||||||
|
- feedback
|
||||||
|
- motion
|
||||||
|
- visual
|
||||||
|
- audio
|
||||||
|
- haptic
|
||||||
|
- binding
|
||||||
@@ -12,6 +12,7 @@ README 面向准备使用 GSP 语言的人类和 AI。它说明 GSP 的用途、
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `README.md` | GSP 语言使用前说明。给人类和 AI 阅读。 |
|
| `README.md` | GSP 语言使用前说明。给人类和 AI 阅读。 |
|
||||||
| `gsp.schema.json` | GSP 第一版核心字段规范。使用 JSON Schema 表达,便于 AI、工具、编译器和实现模块识别。 |
|
| `gsp.schema.json` | GSP 第一版核心字段规范。使用 JSON Schema 表达,便于 AI、工具、编译器和实现模块识别。 |
|
||||||
|
| `gsp.manifest.schema.json` | GSP 工程 manifest 字段规范。 |
|
||||||
|
|
||||||
## GSP 是什么
|
## GSP 是什么
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ id: feedback.positive
|
|||||||
|
|
||||||
只有 `id` 的 GSP 是占位声明。它表示该设计对象存在,但尚未被细化。
|
只有 `id` 的 GSP 是占位声明。它表示该设计对象存在,但尚未被细化。
|
||||||
|
|
||||||
|
默认情况下,`.gsp` 文件放在 GSP 工程根目录的 `design/` 目录下。
|
||||||
|
|
||||||
## 基础 GSP
|
## 基础 GSP
|
||||||
|
|
||||||
`context` 用于写入核心设计内容。
|
`context` 用于写入核心设计内容。
|
||||||
|
|||||||
62
language/gsp.manifest.schema.json
Normal file
62
language/gsp.manifest.schema.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://gsp.local/schema/gsp.manifest.schema.json",
|
||||||
|
"title": "GSP Manifest",
|
||||||
|
"description": "Game Specification Protocol project manifest schema.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"gspVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "GSP language version used by this project."
|
||||||
|
},
|
||||||
|
"toolkitVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expected GSP Toolkit version."
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project id or name."
|
||||||
|
},
|
||||||
|
"entry": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"description": "Entry GSP ids."
|
||||||
|
},
|
||||||
|
"scan": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"description": "Directories or files scanned by the Toolkit. Defaults to design."
|
||||||
|
},
|
||||||
|
"stageRules": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"minResolution": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["L0", "L1", "L2", "L3", "L4", "L5"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Default Toolkit output directory."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,13 +26,25 @@ GSP Toolkit 是 GSP 的配套工具集。
|
|||||||
|
|
||||||
`.gsp` 文件内部遵循 YAML 风格格式。
|
`.gsp` 文件内部遵循 YAML 风格格式。
|
||||||
|
|
||||||
|
默认工程结构:
|
||||||
|
|
||||||
|
```text
|
||||||
|
project/
|
||||||
|
gsp.manifest
|
||||||
|
design/
|
||||||
|
*.gsp
|
||||||
|
```
|
||||||
|
|
||||||
|
未提供 manifest 时,Toolkit 默认扫描工程根目录下的 `design/`。
|
||||||
|
|
||||||
严格字段规范参考:
|
严格字段规范参考:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
../language/gsp.schema.json
|
../language/gsp.schema.json
|
||||||
|
../language/gsp.manifest.schema.json
|
||||||
```
|
```
|
||||||
|
|
||||||
项目级入口配置待讨论,暂定方向:
|
项目级入口配置:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
gsp.manifest
|
gsp.manifest
|
||||||
|
|||||||
@@ -28,26 +28,45 @@ func LoadProject(root string) (*Project, error) {
|
|||||||
ByID: map[string]*Unit{},
|
ByID: map[string]*Unit{},
|
||||||
Duplicates: map[string][]*Unit{},
|
Duplicates: map[string][]*Unit{},
|
||||||
}
|
}
|
||||||
|
manifest, err := loadManifest(absRoot)
|
||||||
|
if err != nil {
|
||||||
|
project.LoadIssues = append(project.LoadIssues, Issue{Level: "error", Code: "manifest_error", File: "gsp.manifest", Message: err.Error()})
|
||||||
|
}
|
||||||
|
project.Manifest = manifest
|
||||||
|
|
||||||
var files []string
|
var files []string
|
||||||
err = filepath.WalkDir(absRoot, func(path string, entry os.DirEntry, walkErr error) error {
|
for _, scanEntry := range project.Manifest.scanEntries() {
|
||||||
if walkErr != nil {
|
scanPath := filepath.Join(absRoot, filepath.FromSlash(scanEntry))
|
||||||
project.LoadIssues = append(project.LoadIssues, Issue{Level: "error", Code: "walk_error", File: path, Message: walkErr.Error()})
|
info, statErr := os.Stat(scanPath)
|
||||||
return nil
|
if statErr != nil {
|
||||||
|
project.LoadIssues = append(project.LoadIssues, Issue{Level: "warning", Code: "scan_missing", File: scanEntry, Message: "scan path does not exist"})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if entry.IsDir() {
|
if !info.IsDir() {
|
||||||
if ignoredDirs[entry.Name()] {
|
if strings.EqualFold(filepath.Ext(scanPath), ".gsp") {
|
||||||
return filepath.SkipDir
|
files = append(files, scanPath)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
walkErr := filepath.WalkDir(scanPath, func(path string, entry os.DirEntry, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
project.LoadIssues = append(project.LoadIssues, Issue{Level: "error", Code: "walk_error", File: path, Message: walkErr.Error()})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if entry.IsDir() {
|
||||||
|
if ignoredDirs[entry.Name()] {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.EqualFold(filepath.Ext(entry.Name()), ".gsp") {
|
||||||
|
files = append(files, path)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
})
|
||||||
|
if walkErr != nil {
|
||||||
|
return nil, walkErr
|
||||||
}
|
}
|
||||||
if strings.EqualFold(filepath.Ext(entry.Name()), ".gsp") {
|
|
||||||
files = append(files, path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
sort.Strings(files)
|
sort.Strings(files)
|
||||||
|
|
||||||
|
|||||||
58
toolkit/internal/gsp/manifest.go
Normal file
58
toolkit/internal/gsp/manifest.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package gsp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manifest struct {
|
||||||
|
GSPVersion string `json:"gspVersion,omitempty" yaml:"gspVersion"`
|
||||||
|
ToolkitVersion string `json:"toolkitVersion,omitempty" yaml:"toolkitVersion"`
|
||||||
|
Project string `json:"project,omitempty" yaml:"project"`
|
||||||
|
Entry []string `json:"entry,omitempty" yaml:"entry"`
|
||||||
|
Scan []string `json:"scan,omitempty" yaml:"scan"`
|
||||||
|
StageRules map[string]StageRule `json:"stageRules,omitempty" yaml:"stageRules"`
|
||||||
|
Types []string `json:"types,omitempty" yaml:"types"`
|
||||||
|
Output string `json:"output,omitempty" yaml:"output"`
|
||||||
|
File string `json:"file,omitempty" yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StageRule struct {
|
||||||
|
MinResolution string `json:"minResolution,omitempty" yaml:"minResolution"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadManifest(root string) (*Manifest, error) {
|
||||||
|
path := filepath.Join(root, "gsp.manifest")
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var manifest Manifest
|
||||||
|
if err := yaml.Unmarshal(data, &manifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
manifest.File = relPath(root, path)
|
||||||
|
return &manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manifest) scanEntries() []string {
|
||||||
|
if m == nil || len(m.Scan) == 0 {
|
||||||
|
return []string{"design"}
|
||||||
|
}
|
||||||
|
return m.Scan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manifest) minResolution(stage string) (string, bool) {
|
||||||
|
if m != nil && m.StageRules != nil {
|
||||||
|
if rule, ok := m.StageRules[stage]; ok && rule.MinResolution != "" {
|
||||||
|
return rule.MinResolution, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value, ok := defaultStageRules[stage]
|
||||||
|
return value, ok
|
||||||
|
}
|
||||||
@@ -96,6 +96,7 @@ func (r *Report) addNotice(code, id, file, message string) {
|
|||||||
|
|
||||||
type Project struct {
|
type Project struct {
|
||||||
Root string
|
Root string
|
||||||
|
Manifest *Manifest
|
||||||
Units []*Unit
|
Units []*Unit
|
||||||
ByID map[string]*Unit
|
ByID map[string]*Unit
|
||||||
Duplicates map[string][]*Unit
|
Duplicates map[string][]*Unit
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ import (
|
|||||||
func (p *Project) Validate(stage string) Report {
|
func (p *Project) Validate(stage string) Report {
|
||||||
report := Report{OK: true}
|
report := Report{OK: true}
|
||||||
for _, issue := range p.LoadIssues {
|
for _, issue := range p.LoadIssues {
|
||||||
report.Errors = append(report.Errors, issue)
|
switch issue.Level {
|
||||||
report.OK = false
|
case "error":
|
||||||
|
report.Errors = append(report.Errors, issue)
|
||||||
|
report.OK = false
|
||||||
|
case "warning":
|
||||||
|
report.Warnings = append(report.Warnings, issue)
|
||||||
|
default:
|
||||||
|
report.Notices = append(report.Notices, issue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, unit := range p.Units {
|
for _, unit := range p.Units {
|
||||||
if unit.ID == "" {
|
if unit.ID == "" {
|
||||||
@@ -81,15 +88,17 @@ func (p *Project) Index() []IndexEntry {
|
|||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultStageRules = map[string]string{
|
||||||
|
"design": "L0",
|
||||||
|
"integrate": "L2",
|
||||||
|
"implement": "L3",
|
||||||
|
"bind": "L4",
|
||||||
|
"release": "L5",
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Project) StageCheck(stage string) Report {
|
func (p *Project) StageCheck(stage string) Report {
|
||||||
report := Report{OK: true}
|
report := Report{OK: true}
|
||||||
required, ok := map[string]string{
|
required, ok := p.Manifest.minResolution(stage)
|
||||||
"design": "L0",
|
|
||||||
"integrate": "L2",
|
|
||||||
"implement": "L3",
|
|
||||||
"bind": "L4",
|
|
||||||
"release": "L5",
|
|
||||||
}[stage]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
report.addError("unknown_stage", "", "", fmt.Sprintf("unknown stage %q", stage))
|
report.addError("unknown_stage", "", "", fmt.Sprintf("unknown stage %q", stage))
|
||||||
return report
|
return report
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import (
|
|||||||
|
|
||||||
func TestLoadValidateAndFlatten(t *testing.T) {
|
func TestLoadValidateAndFlatten(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
writeTestFile(t, root, "page.gsp", `id: page.lottery.main
|
design := filepath.Join(root, "design")
|
||||||
|
if err := os.MkdirAll(design, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeTestFile(t, design, "page.gsp", `id: page.lottery.main
|
||||||
type: page
|
type: page
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: Lottery page.
|
context: Lottery page.
|
||||||
@@ -18,17 +22,17 @@ with:
|
|||||||
- feedback.positive
|
- feedback.positive
|
||||||
refines: page.lottery.base
|
refines: page.lottery.base
|
||||||
`)
|
`)
|
||||||
writeTestFile(t, root, "button.gsp", `id: ui.button.primary
|
writeTestFile(t, design, "button.gsp", `id: ui.button.primary
|
||||||
type: ui
|
type: ui
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: Primary button.
|
context: Primary button.
|
||||||
`)
|
`)
|
||||||
writeTestFile(t, root, "feedback.gsp", `id: feedback.positive
|
writeTestFile(t, design, "feedback.gsp", `id: feedback.positive
|
||||||
type: feedback
|
type: feedback
|
||||||
resolution: L2
|
resolution: L2
|
||||||
context: Positive feedback.
|
context: Positive feedback.
|
||||||
`)
|
`)
|
||||||
writeTestFile(t, root, "base.gsp", `id: page.lottery.base
|
writeTestFile(t, design, "base.gsp", `id: page.lottery.base
|
||||||
resolution: L2
|
resolution: L2
|
||||||
context: Base lottery page.
|
context: Base lottery page.
|
||||||
`)
|
`)
|
||||||
@@ -52,7 +56,11 @@ context: Base lottery page.
|
|||||||
|
|
||||||
func TestValidateMissingReference(t *testing.T) {
|
func TestValidateMissingReference(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
writeTestFile(t, root, "page.gsp", `id: page.missing
|
design := filepath.Join(root, "design")
|
||||||
|
if err := os.MkdirAll(design, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeTestFile(t, design, "page.gsp", `id: page.missing
|
||||||
with:
|
with:
|
||||||
- ui.missing
|
- ui.missing
|
||||||
`)
|
`)
|
||||||
@@ -77,7 +85,11 @@ with:
|
|||||||
|
|
||||||
func TestStageCheck(t *testing.T) {
|
func TestStageCheck(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
writeTestFile(t, root, "page.gsp", `id: page.low
|
design := filepath.Join(root, "design")
|
||||||
|
if err := os.MkdirAll(design, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeTestFile(t, design, "page.gsp", `id: page.low
|
||||||
resolution: L2
|
resolution: L2
|
||||||
`)
|
`)
|
||||||
project, err := LoadProject(root)
|
project, err := LoadProject(root)
|
||||||
|
|||||||
Reference in New Issue
Block a user