Add graph markdown and canvas outputs
This commit is contained in:
@@ -50,6 +50,8 @@ bin/gsp.exe
|
|||||||
.\bin\gsp.exe flatten page.lottery.main --root examples\lottery --depth -1 --out .gsp\flattened.json
|
.\bin\gsp.exe flatten page.lottery.main --root examples\lottery --depth -1 --out .gsp\flattened.json
|
||||||
.\bin\gsp.exe pack page.lottery.main --root examples\lottery --depth -1 --budget 12000 --out .gsp\context-pack.json
|
.\bin\gsp.exe pack page.lottery.main --root examples\lottery --depth -1 --budget 12000 --out .gsp\context-pack.json
|
||||||
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format mermaid --out .gsp\graph.mmd
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format mermaid --out .gsp\graph.mmd
|
||||||
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format md --out .gsp\graph.md
|
||||||
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format canvas --out .gsp\graph.canvas
|
||||||
.\bin\gsp.exe stage-check --root examples\lottery --stage implement --out .gsp\stage-report.json
|
.\bin\gsp.exe stage-check --root examples\lottery --stage implement --out .gsp\stage-report.json
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,6 +63,8 @@ bin/gsp.exe
|
|||||||
.gsp/flattened.json
|
.gsp/flattened.json
|
||||||
.gsp/context-pack.json
|
.gsp/context-pack.json
|
||||||
.gsp/graph.mmd
|
.gsp/graph.mmd
|
||||||
|
.gsp/graph.md
|
||||||
|
.gsp/graph.canvas
|
||||||
.gsp/stage-report.json
|
.gsp/stage-report.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,6 @@ page.lottery.main
|
|||||||
.\bin\gsp.exe validate --root examples\lottery
|
.\bin\gsp.exe validate --root examples\lottery
|
||||||
.\bin\gsp.exe flatten page.lottery.main --root examples\lottery --depth -1
|
.\bin\gsp.exe flatten page.lottery.main --root examples\lottery --depth -1
|
||||||
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format mermaid
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format mermaid
|
||||||
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format md --out .gsp\lottery-graph.md
|
||||||
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format canvas --out .gsp\lottery-graph.canvas
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
id: audio.reward.pop
|
id: audio.reward.pop
|
||||||
|
title: 奖励弹出音效
|
||||||
type: audio
|
type: audio
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 奖励弹出音效,用于强化奖励出现的瞬间反馈。
|
context: 奖励弹出音效,用于强化奖励出现的瞬间反馈。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
id: feedback.positive
|
id: feedback.positive
|
||||||
|
title: 积极反馈
|
||||||
type: feedback
|
type: feedback
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 积极反馈。用于让玩家在操作后获得明确、正向、值得继续的感受。
|
context: 积极反馈。用于让玩家在操作后获得明确、正向、值得继续的感受。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
id: mechanic.lottery.basic
|
id: mechanic.lottery.basic
|
||||||
|
title: 基础抽奖机制
|
||||||
type: mechanic
|
type: mechanic
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 基础抽奖机制,负责抽取请求、奖励结果和结果反馈触发。
|
context: 基础抽奖机制,负责抽取请求、奖励结果和结果反馈触发。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
id: motion.button.reward_pop
|
id: motion.button.reward_pop
|
||||||
|
title: 奖励按钮点击动效
|
||||||
type: motion
|
type: motion
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 奖励按钮点击动效,表现为轻微缩放和快速回弹。
|
context: 奖励按钮点击动效,表现为轻微缩放和快速回弹。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
id: page.lottery.main
|
id: page.lottery.main
|
||||||
|
title: 抽奖页面
|
||||||
type: page
|
type: page
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 抽奖页面,需要表达奖励期待、抽取行为和结果反馈。玩家应快速理解奖池价值、抽奖入口和结果反馈。
|
context: 抽奖页面,需要表达奖励期待、抽取行为和结果反馈。玩家应快速理解奖池价值、抽奖入口和结果反馈。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
id: style.reward.light
|
id: style.reward.light
|
||||||
|
title: 轻快奖励风格
|
||||||
type: style
|
type: style
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 奖励表现要轻快、积极,但不要过度刺激。
|
context: 奖励表现要轻快、积极,但不要过度刺激。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
id: ui.button.primary
|
id: ui.button.primary
|
||||||
|
title: 通用主按钮
|
||||||
type: ui
|
type: ui
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 通用主按钮,用于明确、正向、优先级最高的玩家操作。
|
context: 通用主按钮,用于明确、正向、优先级最高的玩家操作。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
id: ui.button.reward_primary
|
id: ui.button.reward_primary
|
||||||
|
title: 奖励主按钮
|
||||||
type: ui
|
type: ui
|
||||||
resolution: L3
|
resolution: L3
|
||||||
refines: ui.button.primary
|
refines: ui.button.primary
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ GSP 是一种游戏规格协议,不是具体游戏引擎、代码框架或资
|
|||||||
- 一个集合或范围
|
- 一个集合或范围
|
||||||
- 一个逐步细化中的设计对象
|
- 一个逐步细化中的设计对象
|
||||||
|
|
||||||
GSP 不预设抽象和实体的硬边界。所有对象都先被视为 GSP,再由 `context`、`resolution`、`with`、`refines` 和当前任务共同解释。
|
GSP 不预设抽象和实体的硬边界。所有对象都先被视为 GSP,再由 `title`、`context`、`resolution`、`with`、`refines` 和当前任务共同解释。
|
||||||
|
|
||||||
## GSP 用来做什么
|
## GSP 用来做什么
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ context: 积极反馈。用于让玩家在操作后获得明确、正向、值
|
|||||||
| 字段 | 必需 | 作用 |
|
| 字段 | 必需 | 作用 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `id` | 是 | 唯一身份。 |
|
| `id` | 是 | 唯一身份。 |
|
||||||
|
| `title` | 否 | 展示标题。 |
|
||||||
| `context` | 否 | 核心设计内容。 |
|
| `context` | 否 | 核心设计内容。 |
|
||||||
| `resolution` | 否 | 清晰度 / 细化程度。 |
|
| `resolution` | 否 | 清晰度 / 细化程度。 |
|
||||||
| `with` | 否 | 通用设计语境关系。 |
|
| `with` | 否 | 通用设计语境关系。 |
|
||||||
@@ -86,6 +87,7 @@ context: 积极反馈。用于让玩家在操作后获得明确、正向、值
|
|||||||
|
|
||||||
- GSP 是单文件、单格式的设计协议。
|
- GSP 是单文件、单格式的设计协议。
|
||||||
- `id` 是唯一强制字段。
|
- `id` 是唯一强制字段。
|
||||||
|
- `title` 是展示标题。
|
||||||
- `context` 可选。
|
- `context` 可选。
|
||||||
- `resolution` 是清晰度软指标。
|
- `resolution` 是清晰度软指标。
|
||||||
- `with` 是通用设计语境关系。
|
- `with` 是通用设计语境关系。
|
||||||
@@ -109,6 +111,17 @@ context: 积极反馈。用于让玩家在操作后获得明确、正向、值
|
|||||||
|
|
||||||
编译器可以按阶段检查 `resolution`,但 `resolution` 不证明设计质量。
|
编译器可以按阶段检查 `resolution`,但 `resolution` 不证明设计质量。
|
||||||
|
|
||||||
|
## title
|
||||||
|
|
||||||
|
`title` 表示 GSP 的展示标题。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: page.lottery.main
|
||||||
|
title: 抽奖页面
|
||||||
|
```
|
||||||
|
|
||||||
|
工具在图形、索引和 AI 入口中优先使用 `title` 展示。没有 `title` 时使用 `id`。
|
||||||
|
|
||||||
## with
|
## with
|
||||||
|
|
||||||
`with` 表示当前 GSP 需要与哪些 GSP 一起进入设计语境。
|
`with` 表示当前 GSP 需要与哪些 GSP 一起进入设计语境。
|
||||||
@@ -163,8 +176,9 @@ context: 奖励表现要轻快、积极,但不要过度刺激。
|
|||||||
使用 GSP 时按以下顺序理解:
|
使用 GSP 时按以下顺序理解:
|
||||||
|
|
||||||
1. 先读 `id`,确认身份。
|
1. 先读 `id`,确认身份。
|
||||||
2. 再读 `context`,理解核心设计语义。
|
2. 再读 `title`,确认展示名称。
|
||||||
3. 查看 `resolution`,判断当前细化程度。
|
3. 再读 `context`,理解核心设计语义。
|
||||||
4. 展开 `with`,补齐相关设计语境。
|
4. 查看 `resolution`,判断当前细化程度。
|
||||||
5. 查看 `refines`,确认是否来自某个更早或更粗的 GSP。
|
5. 展开 `with`,补齐相关设计语境。
|
||||||
6. 需要严格校验时使用 `gsp.schema.json`。
|
6. 查看 `refines`,确认是否来自某个更早或更粗的 GSP。
|
||||||
|
7. 需要严格校验时使用 `gsp.schema.json`。
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
- `.gsp` files use YAML.
|
- `.gsp` files use YAML.
|
||||||
- Preserve `id`; do not rename it unless explicitly requested.
|
- Preserve `id`; do not rename it unless explicitly requested.
|
||||||
- `id` is the unique identity of a GSP unit.
|
- `id` is the unique identity of a GSP unit.
|
||||||
|
- `title` is display text; use `id` when `title` is missing.
|
||||||
- Use only fields valid for the declared GSP version.
|
- Use only fields valid for the declared GSP version.
|
||||||
- `with` means related design context.
|
- `with` means related design context.
|
||||||
- `refines` means single-source refinement.
|
- `refines` means single-source refinement.
|
||||||
|
|||||||
@@ -94,9 +94,20 @@ gsp pack <id> [--root .] [--depth 3] [--budget 12000] [--out context-pack.json]
|
|||||||
Generate a relation graph.
|
Generate a relation graph.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gsp graph [id] [--root .] [--depth 3] [--format json|mermaid] [--out graph.json]
|
gsp graph [id] [--root .] [--depth 3] [--format json|mermaid|md|canvas] [--out graph.json]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Formats:
|
||||||
|
|
||||||
|
```text
|
||||||
|
json Machine-readable graph.
|
||||||
|
mermaid Mermaid graph body.
|
||||||
|
md Markdown file with Mermaid block.
|
||||||
|
canvas Obsidian JSON Canvas file.
|
||||||
|
```
|
||||||
|
|
||||||
|
Graph display uses `title` when present and falls back to `id`.
|
||||||
|
|
||||||
## stage-check
|
## stage-check
|
||||||
|
|
||||||
Check minimum resolution for a stage.
|
Check minimum resolution for a stage.
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"description": "Unique GSP id in the current project."
|
"description": "Unique GSP id in the current project."
|
||||||
},
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional human-readable display title. Tools use id when title is missing."
|
||||||
|
},
|
||||||
"context": {
|
"context": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Core design context. Optional. Empty or missing context means the GSP can be treated as a placeholder."
|
"description": "Core design context. Optional. Empty or missing context means the GSP can be treated as a placeholder."
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ manifest 可用于声明:
|
|||||||
索引内容:
|
索引内容:
|
||||||
|
|
||||||
- `id`
|
- `id`
|
||||||
|
- `title`
|
||||||
- 文件路径
|
- 文件路径
|
||||||
- `type`
|
- `type`
|
||||||
- `resolution`
|
- `resolution`
|
||||||
@@ -138,6 +139,8 @@ manifest 可用于声明:
|
|||||||
|
|
||||||
- JSON 图数据
|
- JSON 图数据
|
||||||
- Mermaid 图
|
- Mermaid 图
|
||||||
|
- Markdown Mermaid 图
|
||||||
|
- Obsidian Canvas 图
|
||||||
|
|
||||||
图类型:
|
图类型:
|
||||||
|
|
||||||
@@ -187,6 +190,8 @@ go build -o ../bin/gsp ./cmd/gsp
|
|||||||
../bin/gsp validate --root ../examples/lottery
|
../bin/gsp validate --root ../examples/lottery
|
||||||
../bin/gsp flatten page.lottery.main --root ../examples/lottery --depth -1 --out ../.gsp/flattened.json
|
../bin/gsp flatten page.lottery.main --root ../examples/lottery --depth -1 --out ../.gsp/flattened.json
|
||||||
../bin/gsp graph page.lottery.main --root ../examples/lottery --format mermaid --out ../.gsp/graph.mmd
|
../bin/gsp graph page.lottery.main --root ../examples/lottery --format mermaid --out ../.gsp/graph.mmd
|
||||||
|
../bin/gsp graph page.lottery.main --root ../examples/lottery --format md --out ../.gsp/graph.md
|
||||||
|
../bin/gsp graph page.lottery.main --root ../examples/lottery --format canvas --out ../.gsp/graph.canvas
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. 输出
|
## 5. 输出
|
||||||
@@ -206,6 +211,8 @@ go build -o ../bin/gsp ./cmd/gsp
|
|||||||
.gsp/report.json
|
.gsp/report.json
|
||||||
.gsp/graph.json
|
.gsp/graph.json
|
||||||
.gsp/graph.mmd
|
.gsp/graph.mmd
|
||||||
|
.gsp/graph.md
|
||||||
|
.gsp/graph.canvas
|
||||||
.gsp/context-pack.json
|
.gsp/context-pack.json
|
||||||
.gsp/flattened.json
|
.gsp/flattened.json
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ Usage:
|
|||||||
gsp trace <id> [--root .] [--depth 3] [--out trace.json]
|
gsp trace <id> [--root .] [--depth 3] [--out trace.json]
|
||||||
gsp flatten <id> [--root .] [--depth 3] [--include-type a,b] [--exclude-type a,b] [--out flattened.json]
|
gsp flatten <id> [--root .] [--depth 3] [--include-type a,b] [--exclude-type a,b] [--out flattened.json]
|
||||||
gsp pack <id> [--root .] [--depth 3] [--budget 12000] [--out context-pack.json]
|
gsp pack <id> [--root .] [--depth 3] [--budget 12000] [--out context-pack.json]
|
||||||
gsp graph [id] [--root .] [--depth 3] [--format json|mermaid] [--out graph.json]
|
gsp graph [id] [--root .] [--depth 3] [--format json|mermaid|md|canvas] [--out graph.json]
|
||||||
gsp stage-check --stage implement [--root .] [--out stage-report.json]
|
gsp stage-check --stage implement [--root .] [--out stage-report.json]
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ func runGraph(args []string) error {
|
|||||||
root := commonRoot(fs)
|
root := commonRoot(fs)
|
||||||
out := commonOut(fs)
|
out := commonOut(fs)
|
||||||
depth := fs.Int("depth", 3, "maximum relation depth when id is provided; -1 means unlimited")
|
depth := fs.Int("depth", 3, "maximum relation depth when id is provided; -1 means unlimited")
|
||||||
format := fs.String("format", "json", "json or mermaid")
|
format := fs.String("format", "json", "json, mermaid, md, or canvas")
|
||||||
if err := fs.Parse(normalizeFlagArgs(args)); err != nil {
|
if err := fs.Parse(normalizeFlagArgs(args)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -283,13 +283,22 @@ func runGraph(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
graph := project.Graph(id, *depth)
|
graph := project.Graph(id, *depth)
|
||||||
if *format == "mermaid" {
|
switch *format {
|
||||||
|
case "mermaid":
|
||||||
return writeText(*out, graph.Mermaid())
|
return writeText(*out, graph.Mermaid())
|
||||||
}
|
case "md":
|
||||||
if *format != "json" {
|
return writeText(*out, graph.Markdown())
|
||||||
|
case "canvas":
|
||||||
|
data, err := graph.Canvas()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeBytes(*out, data)
|
||||||
|
case "json":
|
||||||
|
return writeJSON(*out, graph)
|
||||||
|
default:
|
||||||
return fmt.Errorf("unsupported graph format %q", *format)
|
return fmt.Errorf("unsupported graph format %q", *format)
|
||||||
}
|
}
|
||||||
return writeJSON(*out, graph)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runStageCheck(args []string) error {
|
func runStageCheck(args []string) error {
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func aiUsage(gspVersion, scan string) string {
|
|||||||
- `+"`.gsp`"+` files use YAML.
|
- `+"`.gsp`"+` files use YAML.
|
||||||
- Preserve `+"`id`"+`; do not rename it unless explicitly requested.
|
- Preserve `+"`id`"+`; do not rename it unless explicitly requested.
|
||||||
- `+"`id`"+` is the unique identity of a GSP unit.
|
- `+"`id`"+` is the unique identity of a GSP unit.
|
||||||
|
- `+"`title`"+` is display text; use `+"`id`"+` when `+"`title`"+` is missing.
|
||||||
- Use only fields valid for the declared GSP version.
|
- Use only fields valid for the declared GSP version.
|
||||||
- `+"`with`"+` means related design context.
|
- `+"`with`"+` means related design context.
|
||||||
- `+"`refines`"+` means single-source refinement.
|
- `+"`refines`"+` means single-source refinement.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package gsp
|
package gsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -21,7 +22,7 @@ func (p *Project) graphForUnits(units []*Unit) Graph {
|
|||||||
include := map[string]bool{}
|
include := map[string]bool{}
|
||||||
for _, unit := range units {
|
for _, unit := range units {
|
||||||
include[unit.ID] = true
|
include[unit.ID] = true
|
||||||
nodeMap[unit.ID] = GraphNode{ID: unit.ID, Type: unit.Type, Resolution: unit.Resolution, File: unit.File}
|
nodeMap[unit.ID] = GraphNode{ID: unit.ID, Title: unit.Title, Type: unit.Type, Resolution: unit.Resolution, File: unit.File}
|
||||||
}
|
}
|
||||||
for _, unit := range units {
|
for _, unit := range units {
|
||||||
if unit.Refines != "" {
|
if unit.Refines != "" {
|
||||||
@@ -74,7 +75,7 @@ func (g Graph) Mermaid() string {
|
|||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
for _, node := range g.Nodes {
|
for _, node := range g.Nodes {
|
||||||
label := node.ID
|
label := node.DisplayLabel()
|
||||||
if node.Missing {
|
if node.Missing {
|
||||||
label += " (missing)"
|
label += " (missing)"
|
||||||
}
|
}
|
||||||
@@ -86,6 +87,190 @@ func (g Graph) Mermaid() string {
|
|||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g Graph) Markdown() string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("# GSP Graph\n\n")
|
||||||
|
builder.WriteString("```mermaid\n")
|
||||||
|
builder.WriteString(g.Mermaid())
|
||||||
|
builder.WriteString("```\n")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Graph) Canvas() ([]byte, error) {
|
||||||
|
canvas := canvasDocument{
|
||||||
|
Nodes: make([]canvasNode, 0, len(g.Nodes)),
|
||||||
|
Edges: make([]canvasEdge, 0, len(g.Edges)),
|
||||||
|
}
|
||||||
|
positions := g.canvasPositions()
|
||||||
|
for _, node := range g.Nodes {
|
||||||
|
pos := positions[node.ID]
|
||||||
|
canvas.Nodes = append(canvas.Nodes, canvasNode{
|
||||||
|
ID: canvasNodeID(node.ID),
|
||||||
|
Type: "text",
|
||||||
|
Text: node.CanvasText(),
|
||||||
|
X: pos.X,
|
||||||
|
Y: pos.Y,
|
||||||
|
Width: 280,
|
||||||
|
Height: 140,
|
||||||
|
Color: node.CanvasColor(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, edge := range g.Edges {
|
||||||
|
canvas.Edges = append(canvas.Edges, canvasEdge{
|
||||||
|
ID: canvasEdgeID(edge),
|
||||||
|
FromNode: canvasNodeID(edge.From),
|
||||||
|
FromSide: "right",
|
||||||
|
ToNode: canvasNodeID(edge.To),
|
||||||
|
ToSide: "left",
|
||||||
|
Label: edge.Kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(canvas, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(data, '\n'), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n GraphNode) DisplayLabel() string {
|
||||||
|
title := strings.TrimSpace(n.Title)
|
||||||
|
if title != "" {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
return n.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n GraphNode) CanvasText() string {
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, "## "+n.DisplayLabel())
|
||||||
|
if n.Title != "" && n.ID != "" {
|
||||||
|
lines = append(lines, "`"+n.ID+"`")
|
||||||
|
}
|
||||||
|
if n.Type != "" {
|
||||||
|
lines = append(lines, "type: "+n.Type)
|
||||||
|
}
|
||||||
|
if n.Resolution != "" {
|
||||||
|
lines = append(lines, "resolution: "+n.Resolution)
|
||||||
|
}
|
||||||
|
if n.Missing {
|
||||||
|
lines = append(lines, "missing: true")
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n GraphNode) CanvasColor() string {
|
||||||
|
if n.Missing {
|
||||||
|
return "1"
|
||||||
|
}
|
||||||
|
switch n.Resolution {
|
||||||
|
case "L0", "L1":
|
||||||
|
return "2"
|
||||||
|
case "L2":
|
||||||
|
return "3"
|
||||||
|
case "L3":
|
||||||
|
return "4"
|
||||||
|
case "L4", "L5":
|
||||||
|
return "5"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type canvasDocument struct {
|
||||||
|
Nodes []canvasNode `json:"nodes"`
|
||||||
|
Edges []canvasEdge `json:"edges"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type canvasNode struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type canvasEdge struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FromNode string `json:"fromNode"`
|
||||||
|
FromSide string `json:"fromSide,omitempty"`
|
||||||
|
ToNode string `json:"toNode"`
|
||||||
|
ToSide string `json:"toSide,omitempty"`
|
||||||
|
Label string `json:"label,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type canvasPosition struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Graph) canvasPositions() map[string]canvasPosition {
|
||||||
|
incoming := map[string]int{}
|
||||||
|
outgoing := map[string][]string{}
|
||||||
|
for _, node := range g.Nodes {
|
||||||
|
incoming[node.ID] = 0
|
||||||
|
}
|
||||||
|
for _, edge := range g.Edges {
|
||||||
|
if _, ok := incoming[edge.From]; !ok {
|
||||||
|
incoming[edge.From] = 0
|
||||||
|
}
|
||||||
|
if _, ok := incoming[edge.To]; !ok {
|
||||||
|
incoming[edge.To] = 0
|
||||||
|
}
|
||||||
|
incoming[edge.To]++
|
||||||
|
outgoing[edge.From] = append(outgoing[edge.From], edge.To)
|
||||||
|
}
|
||||||
|
for id := range outgoing {
|
||||||
|
sort.Strings(outgoing[id])
|
||||||
|
}
|
||||||
|
var queue []string
|
||||||
|
for _, node := range g.Nodes {
|
||||||
|
if incoming[node.ID] == 0 {
|
||||||
|
queue = append(queue, node.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(queue) == 0 {
|
||||||
|
for _, node := range g.Nodes {
|
||||||
|
queue = append(queue, node.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(queue)
|
||||||
|
depth := map[string]int{}
|
||||||
|
for len(queue) > 0 {
|
||||||
|
id := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
for _, to := range outgoing[id] {
|
||||||
|
if depth[to] < depth[id]+1 {
|
||||||
|
depth[to] = depth[id] + 1
|
||||||
|
}
|
||||||
|
incoming[to]--
|
||||||
|
if incoming[to] == 0 {
|
||||||
|
queue = append(queue, to)
|
||||||
|
sort.Strings(queue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
columns := map[int][]string{}
|
||||||
|
for _, node := range g.Nodes {
|
||||||
|
columns[depth[node.ID]] = append(columns[depth[node.ID]], node.ID)
|
||||||
|
}
|
||||||
|
positions := map[string]canvasPosition{}
|
||||||
|
var keys []int
|
||||||
|
for key := range columns {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Ints(keys)
|
||||||
|
for _, key := range keys {
|
||||||
|
sort.Strings(columns[key])
|
||||||
|
for row, id := range columns[key] {
|
||||||
|
positions[id] = canvasPosition{X: key * 360, Y: row * 190}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return positions
|
||||||
|
}
|
||||||
|
|
||||||
func mermaidID(id string) string {
|
func mermaidID(id string) string {
|
||||||
value := mermaidIDPattern.ReplaceAllString(id, "_")
|
value := mermaidIDPattern.ReplaceAllString(id, "_")
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -101,3 +286,11 @@ func escapeMermaid(value string) string {
|
|||||||
value = strings.ReplaceAll(value, `"`, `\"`)
|
value = strings.ReplaceAll(value, `"`, `\"`)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canvasNodeID(id string) string {
|
||||||
|
return "gsp_" + mermaidID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canvasEdgeID(edge GraphEdge) string {
|
||||||
|
return "edge_" + mermaidID(edge.From) + "_" + mermaidID(edge.Kind) + "_" + mermaidID(edge.To)
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ output: %s
|
|||||||
|
|
||||||
entryFile := filepath.Join(designDir, safeFileName(entry)+".gsp")
|
entryFile := filepath.Join(designDir, safeFileName(entry)+".gsp")
|
||||||
entryContent := fmt.Sprintf(`id: %s
|
entryContent := fmt.Sprintf(`id: %s
|
||||||
|
title: Project Entry
|
||||||
type: concept
|
type: concept
|
||||||
resolution: L0
|
resolution: L0
|
||||||
context: Project entry GSP.
|
context: Project entry GSP.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var resolutionRank = map[string]int{
|
|||||||
|
|
||||||
type Unit struct {
|
type Unit struct {
|
||||||
ID string `json:"id" yaml:"id"`
|
ID string `json:"id" yaml:"id"`
|
||||||
|
Title string `json:"title,omitempty" yaml:"title"`
|
||||||
Context string `json:"context,omitempty" yaml:"context"`
|
Context string `json:"context,omitempty" yaml:"context"`
|
||||||
Resolution string `json:"resolution,omitempty" yaml:"resolution"`
|
Resolution string `json:"resolution,omitempty" yaml:"resolution"`
|
||||||
With Relations `json:"with,omitempty" yaml:"with"`
|
With Relations `json:"with,omitempty" yaml:"with"`
|
||||||
@@ -105,6 +106,7 @@ type Project struct {
|
|||||||
|
|
||||||
type IndexEntry struct {
|
type IndexEntry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Resolution string `json:"resolution,omitempty"`
|
Resolution string `json:"resolution,omitempty"`
|
||||||
@@ -144,6 +146,7 @@ type Graph struct {
|
|||||||
|
|
||||||
type GraphNode struct {
|
type GraphNode struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Resolution string `json:"resolution,omitempty"`
|
Resolution string `json:"resolution,omitempty"`
|
||||||
File string `json:"file,omitempty"`
|
File string `json:"file,omitempty"`
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ func (p *Project) Index() []IndexEntry {
|
|||||||
sort.Strings(with)
|
sort.Strings(with)
|
||||||
entries = append(entries, IndexEntry{
|
entries = append(entries, IndexEntry{
|
||||||
ID: unit.ID,
|
ID: unit.ID,
|
||||||
|
Title: unit.Title,
|
||||||
File: unit.File,
|
File: unit.File,
|
||||||
Type: unit.Type,
|
Type: unit.Type,
|
||||||
Resolution: unit.Resolution,
|
Resolution: unit.Resolution,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package gsp
|
package gsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ func TestLoadValidateAndFlatten(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
writeTestFile(t, design, "page.gsp", `id: page.lottery.main
|
writeTestFile(t, design, "page.gsp", `id: page.lottery.main
|
||||||
|
title: Lottery Page
|
||||||
type: page
|
type: page
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: Lottery page.
|
context: Lottery page.
|
||||||
@@ -52,6 +55,42 @@ context: Base lottery page.
|
|||||||
if !sameStrings(got, want) {
|
if !sameStrings(got, want) {
|
||||||
t.Fatalf("flatten ids = %v, want %v", got, want)
|
t.Fatalf("flatten ids = %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
|
index := project.Index()
|
||||||
|
if index[2].Title != "Lottery Page" {
|
||||||
|
t.Fatalf("index title = %q", index[2].Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
graph := project.Graph("page.lottery.main", -1)
|
||||||
|
if !strings.Contains(graph.Mermaid(), `["Lottery Page"]`) {
|
||||||
|
t.Fatalf("expected Mermaid to use title, got:\n%s", graph.Mermaid())
|
||||||
|
}
|
||||||
|
if !strings.Contains(graph.Markdown(), "```mermaid") {
|
||||||
|
t.Fatalf("expected Markdown Mermaid block")
|
||||||
|
}
|
||||||
|
canvasData, err := graph.Canvas()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var canvas struct {
|
||||||
|
Nodes []struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"nodes"`
|
||||||
|
Edges []struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
} `json:"edges"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(canvasData, &canvas); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
foundTitle := false
|
||||||
|
for _, node := range canvas.Nodes {
|
||||||
|
if strings.Contains(node.Text, "Lottery Page") {
|
||||||
|
foundTitle = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundTitle {
|
||||||
|
t.Fatalf("expected canvas node text to include title, got %+v", canvas.Nodes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateMissingReference(t *testing.T) {
|
func TestValidateMissingReference(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user