Add GSP links field and link inspection
This commit is contained in:
@@ -50,6 +50,7 @@ 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 pack page.lottery.main --root examples\lottery --for implement --stage implement --format md --out .gsp\context-pack.md
|
.\bin\gsp.exe pack page.lottery.main --root examples\lottery --for implement --stage implement --format md --out .gsp\context-pack.md
|
||||||
|
.\bin\gsp.exe links page.lottery.main --root examples\lottery --format md --out .gsp\links.md
|
||||||
.\bin\gsp.exe impact feedback.positive --root examples\lottery --format md --out .gsp\impact.md
|
.\bin\gsp.exe impact feedback.positive --root examples\lottery --format md --out .gsp\impact.md
|
||||||
.\bin\gsp.exe impact feedback.positive --root examples\lottery --format canvas --out .gsp\impact.canvas
|
.\bin\gsp.exe impact feedback.positive --root examples\lottery --format canvas --out .gsp\impact.canvas
|
||||||
.\bin\gsp.exe message validate messages\implement-page.gspmsg --root examples\lottery --out .gsp\message-report.json
|
.\bin\gsp.exe message validate messages\implement-page.gspmsg --root examples\lottery --out .gsp\message-report.json
|
||||||
@@ -68,6 +69,7 @@ bin/gsp.exe
|
|||||||
.gsp/flattened.json
|
.gsp/flattened.json
|
||||||
.gsp/context-pack.json
|
.gsp/context-pack.json
|
||||||
.gsp/context-pack.md
|
.gsp/context-pack.md
|
||||||
|
.gsp/links.md
|
||||||
.gsp/impact.md
|
.gsp/impact.md
|
||||||
.gsp/impact.canvas
|
.gsp/impact.canvas
|
||||||
.gsp/message-report.json
|
.gsp/message-report.json
|
||||||
@@ -155,6 +157,7 @@ gsp validate
|
|||||||
gsp index
|
gsp index
|
||||||
gsp flatten <id>
|
gsp flatten <id>
|
||||||
gsp pack <id> --for implement --format md
|
gsp pack <id> --for implement --format md
|
||||||
|
gsp links <id> --format md
|
||||||
gsp impact <id> --format md
|
gsp impact <id> --format md
|
||||||
gsp message validate message.gspmsg
|
gsp message validate message.gspmsg
|
||||||
gsp graph <id>
|
gsp graph <id>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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 pack page.lottery.main --root examples\lottery --for implement --format md
|
.\bin\gsp.exe pack page.lottery.main --root examples\lottery --for implement --format md
|
||||||
|
.\bin\gsp.exe links page.lottery.main --root examples\lottery --format md
|
||||||
.\bin\gsp.exe impact feedback.positive --root examples\lottery --format md
|
.\bin\gsp.exe impact feedback.positive --root examples\lottery --format md
|
||||||
.\bin\gsp.exe message validate messages\implement-page.gspmsg --root examples\lottery
|
.\bin\gsp.exe message validate messages\implement-page.gspmsg --root examples\lottery
|
||||||
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format mermaid
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format mermaid
|
||||||
|
|||||||
1
examples/lottery/assets/ui/button_primary.txt
Normal file
1
examples/lottery/assets/ui/button_primary.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Primary button visual reference placeholder.
|
||||||
@@ -3,6 +3,10 @@ title: 抽奖页面
|
|||||||
type: page
|
type: page
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 抽奖页面,需要表达奖励期待、抽取行为和结果反馈。玩家应快速理解奖池价值、抽奖入口和结果反馈。
|
context: 抽奖页面,需要表达奖励期待、抽取行为和结果反馈。玩家应快速理解奖池价值、抽奖入口和结果反馈。
|
||||||
|
links:
|
||||||
|
- path: assets/ui
|
||||||
|
role: reference
|
||||||
|
- https://example.com/lottery-reference
|
||||||
with:
|
with:
|
||||||
- ui.button.reward_primary
|
- ui.button.reward_primary
|
||||||
- feedback.positive
|
- feedback.positive
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ title: 通用主按钮
|
|||||||
type: ui
|
type: ui
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: 通用主按钮,用于明确、正向、优先级最高的玩家操作。
|
context: 通用主按钮,用于明确、正向、优先级最高的玩家操作。
|
||||||
|
links: assets/ui/button_primary.txt
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ context: 积极反馈。用于让玩家在操作后获得明确、正向、值
|
|||||||
| `with` | 否 | 通用设计语境关系。 |
|
| `with` | 否 | 通用设计语境关系。 |
|
||||||
| `refines` | 否 | 单一细化来源。 |
|
| `refines` | 否 | 单一细化来源。 |
|
||||||
| `type` | 否 | 辅助分类。 |
|
| `type` | 否 | 辅助分类。 |
|
||||||
|
| `links` | 否 | 关联路径、文件夹、URL 或外部地址。 |
|
||||||
|
|
||||||
## 核心边界
|
## 核心边界
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ context: 积极反馈。用于让玩家在操作后获得明确、正向、值
|
|||||||
- `with` 是通用设计语境关系。
|
- `with` 是通用设计语境关系。
|
||||||
- `refines` 是单一细化来源。
|
- `refines` 是单一细化来源。
|
||||||
- `type` 是辅助分类字段。
|
- `type` 是辅助分类字段。
|
||||||
|
- `links` 是外部对象关联字段。
|
||||||
- GSP 不预设抽象和实体的硬边界。
|
- GSP 不预设抽象和实体的硬边界。
|
||||||
- 置信度、自我纠错、模块信誉和历史归因属于外部模块,不进入 GSP 核心协议。
|
- 置信度、自我纠错、模块信誉和历史归因属于外部模块,不进入 GSP 核心协议。
|
||||||
|
|
||||||
@@ -124,6 +126,40 @@ title: 抽奖页面
|
|||||||
|
|
||||||
工具在图形、索引和 AI 入口中优先使用 `title` 展示。没有 `title` 时使用 `id`。
|
工具在图形、索引和 AI 入口中优先使用 `title` 展示。没有 `title` 时使用 `id`。
|
||||||
|
|
||||||
|
## links
|
||||||
|
|
||||||
|
`links` 表示当前 GSP 关联到的路径、文件夹、URL 或外部地址。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
links: assets/ui/button_primary.png
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
links:
|
||||||
|
- assets/ui/button_primary.png
|
||||||
|
- https://example.com/style-guide
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
links:
|
||||||
|
- path: runtime/ui/RewardButton.prefab
|
||||||
|
role: binding
|
||||||
|
- path: docs/reward-style.md
|
||||||
|
role: reference
|
||||||
|
```
|
||||||
|
|
||||||
|
对象写法只使用 `path`、`role` 和 `context`。`role` 默认是 `reference`。
|
||||||
|
|
||||||
|
内置 `role`:
|
||||||
|
|
||||||
|
| role | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| `reference` | 参考资料。 |
|
||||||
|
| `source` | 原始来源或素材来源。 |
|
||||||
|
| `binding` | 与实现对象绑定。 |
|
||||||
|
| `output` | 输出物。 |
|
||||||
|
| `evidence` | 验收、测试或结论依据。 |
|
||||||
|
|
||||||
## with
|
## with
|
||||||
|
|
||||||
`with` 表示当前 GSP 需要与哪些 GSP 一起进入设计语境。
|
`with` 表示当前 GSP 需要与哪些 GSP 一起进入设计语境。
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- 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.
|
- `title` is display text; use `id` when `title` is missing.
|
||||||
|
- `links` associates a GSP with paths, folders, URLs, or external addresses.
|
||||||
- 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.
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
- Use `gsp trace <id>` to inspect relations.
|
- Use `gsp trace <id>` to inspect relations.
|
||||||
- Use `gsp flatten <id>` before implementation or task splitting.
|
- Use `gsp flatten <id>` before implementation or task splitting.
|
||||||
- Use `gsp pack <id>` when a compact AI context is needed.
|
- Use `gsp pack <id>` when a compact AI context is needed.
|
||||||
|
- Use `gsp links <id>` to inspect associated files, folders, URLs, or addresses.
|
||||||
- Use `gsp impact <id>` before changing shared GSP units.
|
- Use `gsp impact <id>` before changing shared GSP units.
|
||||||
- Use `gsp message validate <file>` for agent communication messages.
|
- Use `gsp message validate <file>` for agent communication messages.
|
||||||
- Use `gsp stage-check --stage <stage>` before stage handoff.
|
- Use `gsp stage-check --stage <stage>` before stage handoff.
|
||||||
|
|||||||
@@ -139,6 +139,21 @@ md Human-readable impact report.
|
|||||||
canvas Obsidian JSON Canvas for affected relations.
|
canvas Obsidian JSON Canvas for affected relations.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## links
|
||||||
|
|
||||||
|
List normalized links from one GSP id and its context.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gsp links <id> [--root .] [--depth -1] [--format json|md] [--out links.json]
|
||||||
|
```
|
||||||
|
|
||||||
|
The tool resolves link kind and status:
|
||||||
|
|
||||||
|
```text
|
||||||
|
kind: url | file | folder | missing | unknown
|
||||||
|
status: ok | missing | invalid | unchecked
|
||||||
|
```
|
||||||
|
|
||||||
## message
|
## message
|
||||||
|
|
||||||
Validate GSP agent communication messages.
|
Validate GSP agent communication messages.
|
||||||
|
|||||||
@@ -59,6 +59,44 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional helper category for search, display, compiler hints and context pruning."
|
"description": "Optional helper category for search, display, compiler hints and context pruning."
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"description": "External paths, folders, URLs, or addresses associated with this GSP.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["path"],
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["reference", "source", "binding", "output", "evidence"]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ gsp index
|
|||||||
gsp trace <id>
|
gsp trace <id>
|
||||||
gsp flatten <id>
|
gsp flatten <id>
|
||||||
gsp pack <id>
|
gsp pack <id>
|
||||||
|
gsp links <id>
|
||||||
gsp impact <id>
|
gsp impact <id>
|
||||||
gsp message validate <file>
|
gsp message validate <file>
|
||||||
gsp graph <id>
|
gsp graph <id>
|
||||||
@@ -193,6 +194,7 @@ 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 pack page.lottery.main --root ../examples/lottery --for implement --format md --out ../.gsp/context-pack.md
|
../bin/gsp pack page.lottery.main --root ../examples/lottery --for implement --format md --out ../.gsp/context-pack.md
|
||||||
|
../bin/gsp links page.lottery.main --root ../examples/lottery --format md --out ../.gsp/links.md
|
||||||
../bin/gsp impact feedback.positive --root ../examples/lottery --format md --out ../.gsp/impact.md
|
../bin/gsp impact feedback.positive --root ../examples/lottery --format md --out ../.gsp/impact.md
|
||||||
../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 md --out ../.gsp/graph.md
|
||||||
@@ -221,6 +223,7 @@ go build -o ../bin/gsp ./cmd/gsp
|
|||||||
.gsp/graph.canvas
|
.gsp/graph.canvas
|
||||||
.gsp/context-pack.json
|
.gsp/context-pack.json
|
||||||
.gsp/context-pack.md
|
.gsp/context-pack.md
|
||||||
|
.gsp/links.md
|
||||||
.gsp/flattened.json
|
.gsp/flattened.json
|
||||||
.gsp/impact.md
|
.gsp/impact.md
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ $script:GspSubcommands = @(
|
|||||||
'trace',
|
'trace',
|
||||||
'flatten',
|
'flatten',
|
||||||
'pack',
|
'pack',
|
||||||
|
'links',
|
||||||
'impact',
|
'impact',
|
||||||
'message',
|
'message',
|
||||||
'graph',
|
'graph',
|
||||||
@@ -29,6 +30,7 @@ $script:GspFlags = @{
|
|||||||
'trace' = @('--root', '--depth', '--out')
|
'trace' = @('--root', '--depth', '--out')
|
||||||
'flatten' = @('--root', '--depth', '--include-type', '--exclude-type', '--out')
|
'flatten' = @('--root', '--depth', '--include-type', '--exclude-type', '--out')
|
||||||
'pack' = @('--root', '--for', '--stage', '--depth', '--budget', '--format', '--include-type', '--exclude-type', '--out')
|
'pack' = @('--root', '--for', '--stage', '--depth', '--budget', '--format', '--include-type', '--exclude-type', '--out')
|
||||||
|
'links' = @('--root', '--depth', '--format', '--out')
|
||||||
'impact' = @('--root', '--depth', '--format', '--out')
|
'impact' = @('--root', '--depth', '--format', '--out')
|
||||||
'message' = @('validate')
|
'message' = @('validate')
|
||||||
'graph' = @('--root', '--depth', '--format', '--out')
|
'graph' = @('--root', '--depth', '--format', '--out')
|
||||||
@@ -80,6 +82,8 @@ Register-ArgumentCompleter -Native -CommandName gsp -ScriptBlock {
|
|||||||
$formats = @('json', 'md', 'canvas')
|
$formats = @('json', 'md', 'canvas')
|
||||||
if ($command -eq 'graph') {
|
if ($command -eq 'graph') {
|
||||||
$formats = @('json', 'mermaid', 'md', 'canvas')
|
$formats = @('json', 'mermaid', 'md', 'canvas')
|
||||||
|
} elseif ($command -eq 'links') {
|
||||||
|
$formats = @('json', 'md')
|
||||||
}
|
}
|
||||||
return $formats |
|
return $formats |
|
||||||
Where-Object { $_ -like "$wordToComplete*" } |
|
Where-Object { $_ -like "$wordToComplete*" } |
|
||||||
@@ -125,7 +129,7 @@ Register-ArgumentCompleter -Native -CommandName gsp -ScriptBlock {
|
|||||||
ForEach-Object { New-GspCompletion $_ }
|
ForEach-Object { New-GspCompletion $_ }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@('trace', 'flatten', 'pack', 'impact', 'graph') -contains $command) {
|
if (@('trace', 'flatten', 'pack', 'links', 'impact', 'graph') -contains $command) {
|
||||||
return Get-GspIds |
|
return Get-GspIds |
|
||||||
Where-Object { $_ -like "$wordToComplete*" } |
|
Where-Object { $_ -like "$wordToComplete*" } |
|
||||||
ForEach-Object { New-GspCompletion $_ }
|
ForEach-Object { New-GspCompletion $_ }
|
||||||
@@ -144,11 +148,13 @@ _gsp_completion() {
|
|||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
cmd="${COMP_WORDS[1]}"
|
cmd="${COMP_WORDS[1]}"
|
||||||
local commands="init ai-init version completion validate index trace flatten pack impact message graph stage-check help"
|
local commands="init ai-init version completion validate index trace flatten pack links impact message graph stage-check help"
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--format)
|
--format)
|
||||||
if [[ "$cmd" == "graph" ]]; then
|
if [[ "$cmd" == "graph" ]]; then
|
||||||
COMPREPLY=( $(compgen -W "json mermaid md canvas" -- "$cur") )
|
COMPREPLY=( $(compgen -W "json mermaid md canvas" -- "$cur") )
|
||||||
|
elif [[ "$cmd" == "links" ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W "json md" -- "$cur") )
|
||||||
else
|
else
|
||||||
COMPREPLY=( $(compgen -W "json md canvas" -- "$cur") )
|
COMPREPLY=( $(compgen -W "json md canvas" -- "$cur") )
|
||||||
fi
|
fi
|
||||||
@@ -171,6 +177,7 @@ _gsp_completion() {
|
|||||||
trace) COMPREPLY=( $(compgen -W "--root --depth --out" -- "$cur") ) ;;
|
trace) COMPREPLY=( $(compgen -W "--root --depth --out" -- "$cur") ) ;;
|
||||||
flatten) COMPREPLY=( $(compgen -W "--root --depth --include-type --exclude-type --out" -- "$cur") ) ;;
|
flatten) COMPREPLY=( $(compgen -W "--root --depth --include-type --exclude-type --out" -- "$cur") ) ;;
|
||||||
pack) COMPREPLY=( $(compgen -W "--root --for --stage --depth --budget --format --include-type --exclude-type --out" -- "$cur") ) ;;
|
pack) COMPREPLY=( $(compgen -W "--root --for --stage --depth --budget --format --include-type --exclude-type --out" -- "$cur") ) ;;
|
||||||
|
links) COMPREPLY=( $(compgen -W "--root --depth --format --out" -- "$cur") ) ;;
|
||||||
impact) COMPREPLY=( $(compgen -W "--root --depth --format --out" -- "$cur") ) ;;
|
impact) COMPREPLY=( $(compgen -W "--root --depth --format --out" -- "$cur") ) ;;
|
||||||
message) COMPREPLY=( $(compgen -W "validate" -- "$cur") ) ;;
|
message) COMPREPLY=( $(compgen -W "validate" -- "$cur") ) ;;
|
||||||
graph) COMPREPLY=( $(compgen -W "--root --depth --format --out" -- "$cur") ) ;;
|
graph) COMPREPLY=( $(compgen -W "--root --depth --format --out" -- "$cur") ) ;;
|
||||||
@@ -180,7 +187,7 @@ _gsp_completion() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
trace|flatten|pack|impact|graph)
|
trace|flatten|pack|links|impact|graph)
|
||||||
local ids
|
local ids
|
||||||
ids=$(gsp index --root . 2>/dev/null | sed -n 's/.*"id": "\([^"]*\)".*/\1/p')
|
ids=$(gsp index --root . 2>/dev/null | sed -n 's/.*"id": "\([^"]*\)".*/\1/p')
|
||||||
COMPREPLY=( $(compgen -W "$ids" -- "$cur") )
|
COMPREPLY=( $(compgen -W "$ids" -- "$cur") )
|
||||||
@@ -206,6 +213,7 @@ _gsp() {
|
|||||||
'trace'
|
'trace'
|
||||||
'flatten'
|
'flatten'
|
||||||
'pack'
|
'pack'
|
||||||
|
'links'
|
||||||
'impact'
|
'impact'
|
||||||
'message'
|
'message'
|
||||||
'graph'
|
'graph'
|
||||||
@@ -220,12 +228,13 @@ _gsp "$@"
|
|||||||
|
|
||||||
func fishCompletionScript() string {
|
func fishCompletionScript() string {
|
||||||
return `# GSP fish completion
|
return `# GSP fish completion
|
||||||
complete -c gsp -f -n '__fish_use_subcommand' -a 'init ai-init version completion validate index trace flatten pack impact message graph stage-check help'
|
complete -c gsp -f -n '__fish_use_subcommand' -a 'init ai-init version completion validate index trace flatten pack links impact message graph stage-check help'
|
||||||
complete -c gsp -n '__fish_seen_subcommand_from graph' -l format -a 'json mermaid md canvas'
|
complete -c gsp -n '__fish_seen_subcommand_from graph' -l format -a 'json mermaid md canvas'
|
||||||
complete -c gsp -n '__fish_seen_subcommand_from pack impact' -l format -a 'json md canvas'
|
complete -c gsp -n '__fish_seen_subcommand_from pack impact' -l format -a 'json md canvas'
|
||||||
|
complete -c gsp -n '__fish_seen_subcommand_from links' -l format -a 'json md'
|
||||||
complete -c gsp -n '__fish_seen_subcommand_from stage-check' -l stage -a 'design integrate implement bind release'
|
complete -c gsp -n '__fish_seen_subcommand_from stage-check' -l stage -a 'design integrate implement bind release'
|
||||||
complete -c gsp -n '__fish_seen_subcommand_from ai-init' -l skill -a 'generic codex'
|
complete -c gsp -n '__fish_seen_subcommand_from ai-init' -l skill -a 'generic codex'
|
||||||
complete -c gsp -n '__fish_seen_subcommand_from pack' -l for -a 'design implement review test acceptance handoff inspect'
|
complete -c gsp -n '__fish_seen_subcommand_from pack' -l for -a 'design implement review test acceptance handoff inspect'
|
||||||
complete -c gsp -n '__fish_seen_subcommand_from trace flatten pack impact graph' -a '(gsp index --root . 2>/dev/null | string match -r ''"id": "([^"]+)"'' | string replace -r ''.*"id": "([^"]+)".*'' ''$1'')'
|
complete -c gsp -n '__fish_seen_subcommand_from trace flatten pack links impact graph' -a '(gsp index --root . 2>/dev/null | string match -r ''"id": "([^"]+)"'' | string replace -r ''.*"id": "([^"]+)".*'' ''$1'')'
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ func run(args []string) error {
|
|||||||
return runFlatten(args[1:])
|
return runFlatten(args[1:])
|
||||||
case "pack":
|
case "pack":
|
||||||
return runPack(args[1:])
|
return runPack(args[1:])
|
||||||
|
case "links":
|
||||||
|
return runLinks(args[1:])
|
||||||
case "impact":
|
case "impact":
|
||||||
return runImpact(args[1:])
|
return runImpact(args[1:])
|
||||||
case "message":
|
case "message":
|
||||||
@@ -78,6 +80,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 .] [--for implement] [--stage implement] [--depth 3] [--budget 12000] [--format json|md|canvas] [--out context-pack.json]
|
gsp pack <id> [--root .] [--for implement] [--stage implement] [--depth 3] [--budget 12000] [--format json|md|canvas] [--out context-pack.json]
|
||||||
|
gsp links <id> [--root .] [--depth -1] [--format json|md] [--out links.json]
|
||||||
gsp impact <id> [--root .] [--depth -1] [--format json|md|canvas] [--out impact.json]
|
gsp impact <id> [--root .] [--depth -1] [--format json|md|canvas] [--out impact.json]
|
||||||
gsp message validate <file> [--root .] [--out message-report.json]
|
gsp message validate <file> [--root .] [--out message-report.json]
|
||||||
gsp graph [id] [--root .] [--depth 3] [--format json|mermaid|md|canvas] [--out graph.json]
|
gsp graph [id] [--root .] [--depth 3] [--format json|mermaid|md|canvas] [--out graph.json]
|
||||||
@@ -85,6 +88,33 @@ Usage:
|
|||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runLinks(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("links", flag.ContinueOnError)
|
||||||
|
root := commonRoot(fs)
|
||||||
|
out := commonOut(fs)
|
||||||
|
depth := fs.Int("depth", -1, "maximum relation depth; -1 means unlimited")
|
||||||
|
format := fs.String("format", "json", "json or md")
|
||||||
|
if err := fs.Parse(normalizeFlagArgs(args)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fs.NArg() != 1 {
|
||||||
|
return fmt.Errorf("links requires one GSP id")
|
||||||
|
}
|
||||||
|
project, err := gsp.LoadProject(*root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result := project.Links(fs.Arg(0), *depth)
|
||||||
|
switch *format {
|
||||||
|
case "json":
|
||||||
|
return writeJSON(*out, result)
|
||||||
|
case "md":
|
||||||
|
return writeText(*out, result.Markdown())
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported links format %q", *format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runCompletion(args []string) error {
|
func runCompletion(args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("completion requires powershell, bash, zsh, fish, or install powershell")
|
return fmt.Errorf("completion requires powershell, bash, zsh, fish, or install powershell")
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ func aiUsage(gspVersion, scan string) string {
|
|||||||
- 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.
|
- `+"`title`"+` is display text; use `+"`id`"+` when `+"`title`"+` is missing.
|
||||||
|
- `+"`links`"+` associates a GSP with paths, folders, URLs, or external addresses.
|
||||||
- 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.
|
||||||
@@ -116,6 +117,7 @@ func aiUsage(gspVersion, scan string) string {
|
|||||||
- Use `+"`gsp trace <id>`"+` to inspect relations.
|
- Use `+"`gsp trace <id>`"+` to inspect relations.
|
||||||
- Use `+"`gsp flatten <id>`"+` before implementation or task splitting.
|
- Use `+"`gsp flatten <id>`"+` before implementation or task splitting.
|
||||||
- Use `+"`gsp pack <id>`"+` when a compact AI context is needed.
|
- Use `+"`gsp pack <id>`"+` when a compact AI context is needed.
|
||||||
|
- Use `+"`gsp links <id>`"+` to inspect associated files, folders, URLs, or addresses.
|
||||||
- Use `+"`gsp impact <id>`"+` before changing shared GSP units.
|
- Use `+"`gsp impact <id>`"+` before changing shared GSP units.
|
||||||
- Use `+"`gsp message validate <file>`"+` for agent communication messages.
|
- Use `+"`gsp message validate <file>`"+` for agent communication messages.
|
||||||
- Use `+"`gsp stage-check --stage <stage>`"+` before stage handoff.
|
- Use `+"`gsp stage-check --stage <stage>`"+` before stage handoff.
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ func (p PackResult) Markdown() string {
|
|||||||
builder.WriteString(fmt.Sprintf("- Stage: `%s`\n", p.Stage))
|
builder.WriteString(fmt.Sprintf("- Stage: `%s`\n", p.Stage))
|
||||||
}
|
}
|
||||||
builder.WriteString(fmt.Sprintf("- Units: `%d`\n", p.Summary.UnitCount))
|
builder.WriteString(fmt.Sprintf("- Units: `%d`\n", p.Summary.UnitCount))
|
||||||
|
if p.Summary.LinkCount > 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("- Links: `%d`\n", p.Summary.LinkCount))
|
||||||
|
}
|
||||||
if p.Summary.MinResolution != "" {
|
if p.Summary.MinResolution != "" {
|
||||||
builder.WriteString(fmt.Sprintf("- Resolution: `%s` to `%s`\n", p.Summary.MinResolution, p.Summary.MaxResolution))
|
builder.WriteString(fmt.Sprintf("- Resolution: `%s` to `%s`\n", p.Summary.MinResolution, p.Summary.MaxResolution))
|
||||||
}
|
}
|
||||||
@@ -53,6 +56,16 @@ func (p PackResult) Markdown() string {
|
|||||||
}
|
}
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
if len(p.Links) > 0 {
|
||||||
|
builder.WriteString("## Links\n\n")
|
||||||
|
for _, link := range p.Links {
|
||||||
|
builder.WriteString(fmt.Sprintf("- `%s` %s\n", link.Owner, link.Path))
|
||||||
|
builder.WriteString(fmt.Sprintf(" - role: `%s`\n", link.Role))
|
||||||
|
builder.WriteString(fmt.Sprintf(" - kind: `%s`\n", link.Kind))
|
||||||
|
builder.WriteString(fmt.Sprintf(" - status: `%s`\n", link.Status))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
if len(p.Warnings) > 0 {
|
if len(p.Warnings) > 0 {
|
||||||
builder.WriteString("## Warnings\n\n")
|
builder.WriteString("## Warnings\n\n")
|
||||||
for _, warning := range p.Warnings {
|
for _, warning := range p.Warnings {
|
||||||
|
|||||||
143
toolkit/internal/gsp/links.go
Normal file
143
toolkit/internal/gsp/links.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package gsp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var allowedLinkRoles = map[string]bool{
|
||||||
|
"reference": true,
|
||||||
|
"source": true,
|
||||||
|
"binding": true,
|
||||||
|
"output": true,
|
||||||
|
"evidence": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) Links(id string, depth int) LinkResult {
|
||||||
|
flattened := p.Flatten(id, depth, Filter{})
|
||||||
|
links := p.resolveLinks(flattened.Units)
|
||||||
|
summary := summarizeLinks(links)
|
||||||
|
return LinkResult{
|
||||||
|
Entry: id,
|
||||||
|
Depth: depth,
|
||||||
|
Links: links,
|
||||||
|
Summary: summary,
|
||||||
|
Warnings: flattened.Warnings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) resolveLinks(units []*Unit) []ResolvedLink {
|
||||||
|
var result []ResolvedLink
|
||||||
|
for _, unit := range units {
|
||||||
|
for _, link := range unit.Links {
|
||||||
|
result = append(result, p.resolveLink(unit, link))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
if result[i].Owner == result[j].Owner {
|
||||||
|
return result[i].Path < result[j].Path
|
||||||
|
}
|
||||||
|
return result[i].Owner < result[j].Owner
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) resolveLink(unit *Unit, link Link) ResolvedLink {
|
||||||
|
role := strings.TrimSpace(link.Role)
|
||||||
|
if role == "" {
|
||||||
|
role = "reference"
|
||||||
|
}
|
||||||
|
resolved := ResolvedLink{
|
||||||
|
Owner: unit.ID,
|
||||||
|
Title: unit.Title,
|
||||||
|
Path: strings.TrimSpace(link.Path),
|
||||||
|
Role: role,
|
||||||
|
Context: strings.TrimSpace(link.Context),
|
||||||
|
File: unit.File,
|
||||||
|
}
|
||||||
|
resolved.Kind, resolved.Status, resolved.Exists = p.classifyLink(resolved.Path)
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) classifyLink(path string) (kind, status string, exists bool) {
|
||||||
|
if path == "" {
|
||||||
|
return "unknown", "invalid", false
|
||||||
|
}
|
||||||
|
if isURL(path) {
|
||||||
|
parsed, err := url.ParseRequestURI(path)
|
||||||
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
|
return "url", "invalid", false
|
||||||
|
}
|
||||||
|
return "url", "unchecked", false
|
||||||
|
}
|
||||||
|
checkPath := path
|
||||||
|
if !filepath.IsAbs(checkPath) {
|
||||||
|
checkPath = filepath.Join(p.Root, filepath.FromSlash(checkPath))
|
||||||
|
}
|
||||||
|
info, err := os.Stat(checkPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "missing", "missing", false
|
||||||
|
}
|
||||||
|
return "unknown", "invalid", false
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return "folder", "ok", true
|
||||||
|
}
|
||||||
|
return "file", "ok", true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isURL(value string) bool {
|
||||||
|
lower := strings.ToLower(value)
|
||||||
|
return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeLinks(links []ResolvedLink) LinkSummary {
|
||||||
|
summary := LinkSummary{
|
||||||
|
Total: len(links),
|
||||||
|
ByKind: map[string]int{},
|
||||||
|
ByRole: map[string]int{},
|
||||||
|
ByState: map[string]int{},
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
summary.ByKind[link.Kind]++
|
||||||
|
summary.ByRole[link.Role]++
|
||||||
|
summary.ByState[link.Status]++
|
||||||
|
}
|
||||||
|
if len(summary.ByKind) == 0 {
|
||||||
|
summary.ByKind = nil
|
||||||
|
}
|
||||||
|
if len(summary.ByRole) == 0 {
|
||||||
|
summary.ByRole = nil
|
||||||
|
}
|
||||||
|
if len(summary.ByState) == 0 {
|
||||||
|
summary.ByState = nil
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r LinkResult) Markdown() string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("# GSP Links\n\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("- Entry: `%s`\n", r.Entry))
|
||||||
|
builder.WriteString(fmt.Sprintf("- Links: `%d`\n", r.Summary.Total))
|
||||||
|
if len(r.Links) == 0 {
|
||||||
|
builder.WriteString("\nNo links.\n")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
builder.WriteString("\n## Links\n\n")
|
||||||
|
for _, link := range r.Links {
|
||||||
|
builder.WriteString(fmt.Sprintf("- `%s` %s\n", link.Owner, link.Path))
|
||||||
|
builder.WriteString(fmt.Sprintf(" - role: `%s`\n", link.Role))
|
||||||
|
builder.WriteString(fmt.Sprintf(" - kind: `%s`\n", link.Kind))
|
||||||
|
builder.WriteString(fmt.Sprintf(" - status: `%s`\n", link.Status))
|
||||||
|
if link.Context != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf(" - context: %s\n", link.Context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ type Unit struct {
|
|||||||
With Relations `json:"with,omitempty" yaml:"with"`
|
With Relations `json:"with,omitempty" yaml:"with"`
|
||||||
Refines string `json:"refines,omitempty" yaml:"refines"`
|
Refines string `json:"refines,omitempty" yaml:"refines"`
|
||||||
Type string `json:"type,omitempty" yaml:"type"`
|
Type string `json:"type,omitempty" yaml:"type"`
|
||||||
|
Links Links `json:"links,omitempty" yaml:"links"`
|
||||||
File string `json:"file,omitempty" yaml:"-"`
|
File string `json:"file,omitempty" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +68,55 @@ func (r *Relations) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Path string `json:"path" yaml:"path"`
|
||||||
|
Role string `json:"role,omitempty" yaml:"role"`
|
||||||
|
Context string `json:"context,omitempty" yaml:"context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links []Link
|
||||||
|
|
||||||
|
func (l *Links) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if value.Kind == 0 || value.Tag == "!!null" {
|
||||||
|
*l = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch value.Kind {
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
if value.Value == "" {
|
||||||
|
return fmt.Errorf("links item cannot be empty")
|
||||||
|
}
|
||||||
|
*l = Links{{Path: value.Value}}
|
||||||
|
return nil
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
out := make([]Link, 0, len(value.Content))
|
||||||
|
for _, item := range value.Content {
|
||||||
|
switch item.Kind {
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
if item.Value == "" {
|
||||||
|
return fmt.Errorf("links item cannot be empty")
|
||||||
|
}
|
||||||
|
out = append(out, Link{Path: item.Value})
|
||||||
|
case yaml.MappingNode:
|
||||||
|
var link Link
|
||||||
|
if err := item.Decode(&link); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if link.Path == "" {
|
||||||
|
return fmt.Errorf("links object item requires path")
|
||||||
|
}
|
||||||
|
out = append(out, link)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("links item must be a string or object")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*l = out
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("links must be a string or list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
@@ -112,6 +162,7 @@ type IndexEntry struct {
|
|||||||
Resolution string `json:"resolution,omitempty"`
|
Resolution string `json:"resolution,omitempty"`
|
||||||
With []string `json:"with,omitempty"`
|
With []string `json:"with,omitempty"`
|
||||||
Refines string `json:"refines,omitempty"`
|
Refines string `json:"refines,omitempty"`
|
||||||
|
Links []string `json:"links,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlattenResult struct {
|
type FlattenResult struct {
|
||||||
@@ -130,16 +181,17 @@ type TraceResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PackResult struct {
|
type PackResult struct {
|
||||||
Entry string `json:"entry"`
|
Entry string `json:"entry"`
|
||||||
Intent string `json:"intent,omitempty"`
|
Intent string `json:"intent,omitempty"`
|
||||||
Stage string `json:"stage,omitempty"`
|
Stage string `json:"stage,omitempty"`
|
||||||
Depth int `json:"depth"`
|
Depth int `json:"depth"`
|
||||||
Budget int `json:"budget,omitempty"`
|
Budget int `json:"budget,omitempty"`
|
||||||
Truncated bool `json:"truncated"`
|
Truncated bool `json:"truncated"`
|
||||||
Units []*Unit `json:"units"`
|
Units []*Unit `json:"units"`
|
||||||
Summary Summary `json:"summary"`
|
Links []ResolvedLink `json:"links,omitempty"`
|
||||||
ApproxChars int `json:"approxChars"`
|
Summary Summary `json:"summary"`
|
||||||
Warnings []Issue `json:"warnings,omitempty"`
|
ApproxChars int `json:"approxChars"`
|
||||||
|
Warnings []Issue `json:"warnings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Summary struct {
|
type Summary struct {
|
||||||
@@ -148,6 +200,34 @@ type Summary struct {
|
|||||||
MaxResolution string `json:"maxResolution,omitempty"`
|
MaxResolution string `json:"maxResolution,omitempty"`
|
||||||
TypeCounts map[string]int `json:"typeCounts,omitempty"`
|
TypeCounts map[string]int `json:"typeCounts,omitempty"`
|
||||||
MissingCount int `json:"missingCount,omitempty"`
|
MissingCount int `json:"missingCount,omitempty"`
|
||||||
|
LinkCount int `json:"linkCount,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkResult struct {
|
||||||
|
Entry string `json:"entry"`
|
||||||
|
Depth int `json:"depth"`
|
||||||
|
Links []ResolvedLink `json:"links,omitempty"`
|
||||||
|
Summary LinkSummary `json:"summary"`
|
||||||
|
Warnings []Issue `json:"warnings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkSummary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
ByKind map[string]int `json:"byKind,omitempty"`
|
||||||
|
ByRole map[string]int `json:"byRole,omitempty"`
|
||||||
|
ByState map[string]int `json:"byState,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResolvedLink struct {
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Exists bool `json:"exists,omitempty"`
|
||||||
|
Context string `json:"context,omitempty"`
|
||||||
|
File string `json:"file,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImpactResult struct {
|
type ImpactResult struct {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package gsp
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -41,6 +42,22 @@ func (p *Project) Validate(stage string) Report {
|
|||||||
report.addError("missing_with", unit.ID, unit.File, fmt.Sprintf("with references missing GSP %q", rel.ID))
|
report.addError("missing_with", unit.ID, unit.File, fmt.Sprintf("with references missing GSP %q", rel.ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, link := range unit.Links {
|
||||||
|
role := strings.TrimSpace(link.Role)
|
||||||
|
if role != "" && !allowedLinkRoles[role] {
|
||||||
|
report.addError("invalid_link_role", unit.ID, unit.File, fmt.Sprintf("link role %q is not allowed", role))
|
||||||
|
}
|
||||||
|
resolved := p.resolveLink(unit, link)
|
||||||
|
switch resolved.Status {
|
||||||
|
case "missing":
|
||||||
|
report.addWarning("missing_link", unit.ID, unit.File, fmt.Sprintf("link %q does not exist", resolved.Path))
|
||||||
|
case "invalid":
|
||||||
|
report.addError("invalid_link", unit.ID, unit.File, fmt.Sprintf("link %q is invalid", resolved.Path))
|
||||||
|
}
|
||||||
|
if filepath.IsAbs(resolved.Path) {
|
||||||
|
report.addWarning("absolute_link_path", unit.ID, unit.File, fmt.Sprintf("link %q is absolute and reduces portability", resolved.Path))
|
||||||
|
}
|
||||||
|
}
|
||||||
if unit.Refines != "" {
|
if unit.Refines != "" {
|
||||||
if _, ok := p.ByID[unit.Refines]; !ok {
|
if _, ok := p.ByID[unit.Refines]; !ok {
|
||||||
report.addError("missing_refines", unit.ID, unit.File, fmt.Sprintf("refines references missing GSP %q", unit.Refines))
|
report.addError("missing_refines", unit.ID, unit.File, fmt.Sprintf("refines references missing GSP %q", unit.Refines))
|
||||||
@@ -80,7 +97,12 @@ func (p *Project) Index() []IndexEntry {
|
|||||||
for _, rel := range unit.With {
|
for _, rel := range unit.With {
|
||||||
with = append(with, rel.ID)
|
with = append(with, rel.ID)
|
||||||
}
|
}
|
||||||
|
links := make([]string, 0, len(unit.Links))
|
||||||
|
for _, link := range unit.Links {
|
||||||
|
links = append(links, link.Path)
|
||||||
|
}
|
||||||
sort.Strings(with)
|
sort.Strings(with)
|
||||||
|
sort.Strings(links)
|
||||||
entries = append(entries, IndexEntry{
|
entries = append(entries, IndexEntry{
|
||||||
ID: unit.ID,
|
ID: unit.ID,
|
||||||
Title: unit.Title,
|
Title: unit.Title,
|
||||||
@@ -89,6 +111,7 @@ func (p *Project) Index() []IndexEntry {
|
|||||||
Resolution: unit.Resolution,
|
Resolution: unit.Resolution,
|
||||||
With: with,
|
With: with,
|
||||||
Refines: unit.Refines,
|
Refines: unit.Refines,
|
||||||
|
Links: links,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Slice(entries, func(i, j int) bool {
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
@@ -184,6 +207,7 @@ func (p *Project) PackFor(id, intent, stage string, depth, budget int, filter Fi
|
|||||||
units = candidate
|
units = candidate
|
||||||
approx = len(data)
|
approx = len(data)
|
||||||
}
|
}
|
||||||
|
links := p.resolveLinks(units)
|
||||||
return PackResult{
|
return PackResult{
|
||||||
Entry: id,
|
Entry: id,
|
||||||
Intent: intent,
|
Intent: intent,
|
||||||
@@ -192,17 +216,19 @@ func (p *Project) PackFor(id, intent, stage string, depth, budget int, filter Fi
|
|||||||
Budget: budget,
|
Budget: budget,
|
||||||
Truncated: truncated,
|
Truncated: truncated,
|
||||||
Units: units,
|
Units: units,
|
||||||
Summary: summarizeUnits(units, len(flattened.Warnings)),
|
Links: links,
|
||||||
|
Summary: summarizeUnits(units, len(flattened.Warnings), len(links)),
|
||||||
ApproxChars: approx,
|
ApproxChars: approx,
|
||||||
Warnings: flattened.Warnings,
|
Warnings: flattened.Warnings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func summarizeUnits(units []*Unit, missingCount int) Summary {
|
func summarizeUnits(units []*Unit, missingCount, linkCount int) Summary {
|
||||||
summary := Summary{
|
summary := Summary{
|
||||||
UnitCount: len(units),
|
UnitCount: len(units),
|
||||||
TypeCounts: map[string]int{},
|
TypeCounts: map[string]int{},
|
||||||
MissingCount: missingCount,
|
MissingCount: missingCount,
|
||||||
|
LinkCount: linkCount,
|
||||||
}
|
}
|
||||||
min := 99
|
min := 99
|
||||||
max := -1
|
max := -1
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ title: Lottery Page
|
|||||||
type: page
|
type: page
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: Lottery page.
|
context: Lottery page.
|
||||||
|
links:
|
||||||
|
- assets/ui
|
||||||
|
- path: https://example.com/lottery
|
||||||
|
role: reference
|
||||||
with:
|
with:
|
||||||
- id: ui.button.primary
|
- id: ui.button.primary
|
||||||
context: Main action.
|
context: Main action.
|
||||||
@@ -29,6 +33,7 @@ refines: page.lottery.base
|
|||||||
type: ui
|
type: ui
|
||||||
resolution: L3
|
resolution: L3
|
||||||
context: Primary button.
|
context: Primary button.
|
||||||
|
links: assets/ui/button.txt
|
||||||
`)
|
`)
|
||||||
writeTestFile(t, design, "feedback.gsp", `id: feedback.positive
|
writeTestFile(t, design, "feedback.gsp", `id: feedback.positive
|
||||||
type: feedback
|
type: feedback
|
||||||
@@ -39,6 +44,11 @@ context: Positive feedback.
|
|||||||
resolution: L2
|
resolution: L2
|
||||||
context: Base lottery page.
|
context: Base lottery page.
|
||||||
`)
|
`)
|
||||||
|
assets := filepath.Join(root, "assets", "ui")
|
||||||
|
if err := os.MkdirAll(assets, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeTestFile(t, assets, "button.txt", `button reference`)
|
||||||
|
|
||||||
project, err := LoadProject(root)
|
project, err := LoadProject(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -99,6 +109,20 @@ context: Base lottery page.
|
|||||||
if !strings.Contains(pack.Markdown(), "GSP Context Pack") {
|
if !strings.Contains(pack.Markdown(), "GSP Context Pack") {
|
||||||
t.Fatal("expected pack markdown")
|
t.Fatal("expected pack markdown")
|
||||||
}
|
}
|
||||||
|
if pack.Summary.LinkCount != 3 {
|
||||||
|
t.Fatalf("pack link count = %d", pack.Summary.LinkCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
links := project.Links("page.lottery.main", -1)
|
||||||
|
if links.Summary.Total != 3 {
|
||||||
|
t.Fatalf("links total = %d", links.Summary.Total)
|
||||||
|
}
|
||||||
|
if links.Summary.ByKind["folder"] != 1 || links.Summary.ByKind["file"] != 1 || links.Summary.ByKind["url"] != 1 {
|
||||||
|
t.Fatalf("links kind summary = %+v", links.Summary.ByKind)
|
||||||
|
}
|
||||||
|
if !strings.Contains(links.Markdown(), "GSP Links") {
|
||||||
|
t.Fatal("expected links markdown")
|
||||||
|
}
|
||||||
|
|
||||||
impact := project.Impact("feedback.positive", -1)
|
impact := project.Impact("feedback.positive", -1)
|
||||||
if impact.Summary.DirectCount != 1 {
|
if impact.Summary.DirectCount != 1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user