Add context packs impact analysis and message validation
This commit is contained in:
13
README.md
13
README.md
@@ -49,6 +49,10 @@ bin/gsp.exe
|
|||||||
.\bin\gsp.exe index --root examples\lottery --out .gsp\index.json
|
.\bin\gsp.exe index --root examples\lottery --out .gsp\index.json
|
||||||
.\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 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 message validate messages\implement-page.gspmsg --root examples\lottery --out .gsp\message-report.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 md --out .gsp\graph.md
|
||||||
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format canvas --out .gsp\graph.canvas
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format canvas --out .gsp\graph.canvas
|
||||||
@@ -63,6 +67,10 @@ bin/gsp.exe
|
|||||||
.gsp/index.json
|
.gsp/index.json
|
||||||
.gsp/flattened.json
|
.gsp/flattened.json
|
||||||
.gsp/context-pack.json
|
.gsp/context-pack.json
|
||||||
|
.gsp/context-pack.md
|
||||||
|
.gsp/impact.md
|
||||||
|
.gsp/impact.canvas
|
||||||
|
.gsp/message-report.json
|
||||||
.gsp/graph.mmd
|
.gsp/graph.mmd
|
||||||
.gsp/graph.md
|
.gsp/graph.md
|
||||||
.gsp/graph.canvas
|
.gsp/graph.canvas
|
||||||
@@ -77,8 +85,10 @@ bin/gsp.exe
|
|||||||
| `specs/versions/0.1/README.md` | GSP 0.1 语言说明。 |
|
| `specs/versions/0.1/README.md` | GSP 0.1 语言说明。 |
|
||||||
| `specs/versions/0.1/gsp.schema.json` | GSP 0.1 核心字段 schema。 |
|
| `specs/versions/0.1/gsp.schema.json` | GSP 0.1 核心字段 schema。 |
|
||||||
| `specs/versions/0.1/gsp.manifest.schema.json` | GSP 0.1 工程 manifest schema。 |
|
| `specs/versions/0.1/gsp.manifest.schema.json` | GSP 0.1 工程 manifest schema。 |
|
||||||
|
| `specs/versions/0.1/gsp.message.schema.json` | GSP 0.1 agent 通信消息 schema。 |
|
||||||
| `specs/versions/0.1/commands.md` | GSP 0.1 命令规范。 |
|
| `specs/versions/0.1/commands.md` | GSP 0.1 命令规范。 |
|
||||||
| `specs/versions/0.1/ai-usage.md` | GSP 0.1 AI 使用规则。 |
|
| `specs/versions/0.1/ai-usage.md` | GSP 0.1 AI 使用规则。 |
|
||||||
|
| `specs/versions/0.1/message.md` | GSP Message 说明。 |
|
||||||
| `toolkit/README.md` | GSP Toolkit 命令与实现说明。 |
|
| `toolkit/README.md` | GSP Toolkit 命令与实现说明。 |
|
||||||
|
|
||||||
## GSP 工程结构
|
## GSP 工程结构
|
||||||
@@ -144,6 +154,9 @@ gsp completion install powershell
|
|||||||
gsp validate
|
gsp validate
|
||||||
gsp index
|
gsp index
|
||||||
gsp flatten <id>
|
gsp flatten <id>
|
||||||
|
gsp pack <id> --for implement --format md
|
||||||
|
gsp impact <id> --format md
|
||||||
|
gsp message validate message.gspmsg
|
||||||
gsp graph <id>
|
gsp graph <id>
|
||||||
gsp stage-check --stage implement
|
gsp stage-check --stage implement
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ page.lottery.main
|
|||||||
```powershell
|
```powershell
|
||||||
.\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 impact feedback.positive --root examples\lottery --format md
|
||||||
|
.\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
|
||||||
.\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 md --out .gsp\lottery-graph.md
|
||||||
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format canvas --out .gsp\lottery-graph.canvas
|
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format canvas --out .gsp\lottery-graph.canvas
|
||||||
|
|||||||
14
examples/lottery/messages/implement-page.gspmsg
Normal file
14
examples/lottery/messages/implement-page.gspmsg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
gspMessageVersion: 0.1
|
||||||
|
id: msg.implement.lottery_page
|
||||||
|
from: planner
|
||||||
|
to: implementer
|
||||||
|
intent: implement
|
||||||
|
entry: page.lottery.main
|
||||||
|
stage: implement
|
||||||
|
requires:
|
||||||
|
- ui.button.reward_primary
|
||||||
|
- feedback.positive
|
||||||
|
contextPack:
|
||||||
|
mode: implement
|
||||||
|
depth: -1
|
||||||
|
budget: 12000
|
||||||
@@ -13,8 +13,10 @@ 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.manifest.schema.json` | GSP 工程 manifest 字段规范。 |
|
||||||
|
| `gsp.message.schema.json` | GSP agent 通信消息字段规范。 |
|
||||||
| `commands.md` | GSP Toolkit 命令规范。 |
|
| `commands.md` | GSP Toolkit 命令规范。 |
|
||||||
| `ai-usage.md` | GSP 项目 AI 使用规则。 |
|
| `ai-usage.md` | GSP 项目 AI 使用规则。 |
|
||||||
|
| `message.md` | GSP Message 说明。 |
|
||||||
|
|
||||||
## GSP 是什么
|
## GSP 是什么
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,6 @@
|
|||||||
- 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 impact <id>` before changing shared GSP units.
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -100,9 +100,55 @@ gsp flatten <id> [--root .] [--depth 3] [--include-type a,b] [--exclude-type a,b
|
|||||||
Create a compact AI context pack.
|
Create a compact AI context pack.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gsp pack <id> [--root .] [--depth 3] [--budget 12000] [--out context-pack.json]
|
gsp pack <id> [--root .] [--for implement] [--stage implement] [--depth 3] [--budget 12000] [--format json|md|canvas] [--out context-pack.json]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `--for` value describes the task intent:
|
||||||
|
|
||||||
|
```text
|
||||||
|
design
|
||||||
|
implement
|
||||||
|
review
|
||||||
|
test
|
||||||
|
acceptance
|
||||||
|
handoff
|
||||||
|
inspect
|
||||||
|
```
|
||||||
|
|
||||||
|
Formats:
|
||||||
|
|
||||||
|
```text
|
||||||
|
json Machine-readable context pack.
|
||||||
|
md Human-readable and AI-readable context pack.
|
||||||
|
canvas Obsidian JSON Canvas for included GSP relations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## impact
|
||||||
|
|
||||||
|
Find direct and indirect GSPs affected by a GSP id.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gsp impact <id> [--root .] [--depth -1] [--format json|md|canvas] [--out impact.json]
|
||||||
|
```
|
||||||
|
|
||||||
|
Formats:
|
||||||
|
|
||||||
|
```text
|
||||||
|
json Machine-readable impact result.
|
||||||
|
md Human-readable impact report.
|
||||||
|
canvas Obsidian JSON Canvas for affected relations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## message
|
||||||
|
|
||||||
|
Validate GSP agent communication messages.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gsp message validate <file> [--root .] [--out message-report.json]
|
||||||
|
```
|
||||||
|
|
||||||
|
Message files use YAML and reference GSP ids from the project.
|
||||||
|
|
||||||
## graph
|
## graph
|
||||||
|
|
||||||
Generate a relation graph.
|
Generate a relation graph.
|
||||||
|
|||||||
63
specs/versions/0.1/gsp.message.schema.json
Normal file
63
specs/versions/0.1/gsp.message.schema.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://gsp.local/schema/gsp.message.schema.json",
|
||||||
|
"title": "GSP Message",
|
||||||
|
"description": "GSP agent communication message schema.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["gspMessageVersion", "id", "from", "to", "intent", "entry"],
|
||||||
|
"properties": {
|
||||||
|
"gspMessageVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["0.1"]
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"from": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"intent": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["design", "implement", "review", "test", "acceptance", "handoff", "inspect"]
|
||||||
|
},
|
||||||
|
"entry": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"stage": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["design", "integrate", "implement", "bind", "release"]
|
||||||
|
},
|
||||||
|
"requires": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contextPack": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["design", "implement", "review", "test", "acceptance", "handoff", "inspect"]
|
||||||
|
},
|
||||||
|
"depth": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"budget": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
specs/versions/0.1/message.md
Normal file
41
specs/versions/0.1/message.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# GSP Message 0.1
|
||||||
|
|
||||||
|
GSP Message is a lightweight agent communication format.
|
||||||
|
|
||||||
|
It references GSP ids and does not replace `.gsp` files.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
gspMessageVersion: 0.1
|
||||||
|
id: msg.implement.lottery_page
|
||||||
|
from: planner
|
||||||
|
to: implementer
|
||||||
|
intent: implement
|
||||||
|
entry: page.lottery.main
|
||||||
|
stage: implement
|
||||||
|
requires:
|
||||||
|
- ui.button.reward_primary
|
||||||
|
- feedback.positive
|
||||||
|
contextPack:
|
||||||
|
mode: implement
|
||||||
|
depth: -1
|
||||||
|
budget: 12000
|
||||||
|
```
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `gspMessageVersion` | Message protocol version. |
|
||||||
|
| `id` | Message id. |
|
||||||
|
| `from` | Sender agent or module id. |
|
||||||
|
| `to` | Receiver agent or module id. |
|
||||||
|
| `intent` | Task intent. |
|
||||||
|
| `entry` | Entry GSP id. |
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gsp message validate message.gspmsg --root .
|
||||||
|
```
|
||||||
|
|
||||||
|
The validator checks message version, required fields, intent, stage, entry GSP, and required GSP references.
|
||||||
@@ -177,6 +177,8 @@ gsp index
|
|||||||
gsp trace <id>
|
gsp trace <id>
|
||||||
gsp flatten <id>
|
gsp flatten <id>
|
||||||
gsp pack <id>
|
gsp pack <id>
|
||||||
|
gsp impact <id>
|
||||||
|
gsp message validate <file>
|
||||||
gsp graph <id>
|
gsp graph <id>
|
||||||
gsp stage-check --stage implement
|
gsp stage-check --stage implement
|
||||||
```
|
```
|
||||||
@@ -190,6 +192,8 @@ go build -o ../bin/gsp ./cmd/gsp
|
|||||||
../bin/gsp version
|
../bin/gsp version
|
||||||
../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 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
|
||||||
../bin/gsp graph page.lottery.main --root ../examples/lottery --format canvas --out ../.gsp/graph.canvas
|
../bin/gsp graph page.lottery.main --root ../examples/lottery --format canvas --out ../.gsp/graph.canvas
|
||||||
@@ -216,7 +220,9 @@ go build -o ../bin/gsp ./cmd/gsp
|
|||||||
.gsp/graph.md
|
.gsp/graph.md
|
||||||
.gsp/graph.canvas
|
.gsp/graph.canvas
|
||||||
.gsp/context-pack.json
|
.gsp/context-pack.json
|
||||||
|
.gsp/context-pack.md
|
||||||
.gsp/flattened.json
|
.gsp/flattened.json
|
||||||
|
.gsp/impact.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6. 必须考虑的问题
|
## 6. 必须考虑的问题
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ $script:GspSubcommands = @(
|
|||||||
'trace',
|
'trace',
|
||||||
'flatten',
|
'flatten',
|
||||||
'pack',
|
'pack',
|
||||||
|
'impact',
|
||||||
|
'message',
|
||||||
'graph',
|
'graph',
|
||||||
'stage-check',
|
'stage-check',
|
||||||
'help'
|
'help'
|
||||||
@@ -26,7 +28,9 @@ $script:GspFlags = @{
|
|||||||
'index' = @('--root', '--out')
|
'index' = @('--root', '--out')
|
||||||
'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', '--depth', '--budget', '--include-type', '--exclude-type', '--out')
|
'pack' = @('--root', '--for', '--stage', '--depth', '--budget', '--format', '--include-type', '--exclude-type', '--out')
|
||||||
|
'impact' = @('--root', '--depth', '--format', '--out')
|
||||||
|
'message' = @('validate')
|
||||||
'graph' = @('--root', '--depth', '--format', '--out')
|
'graph' = @('--root', '--depth', '--format', '--out')
|
||||||
'stage-check' = @('--stage', '--root', '--out')
|
'stage-check' = @('--stage', '--root', '--out')
|
||||||
}
|
}
|
||||||
@@ -73,7 +77,11 @@ Register-ArgumentCompleter -Native -CommandName gsp -ScriptBlock {
|
|||||||
|
|
||||||
switch ($previous) {
|
switch ($previous) {
|
||||||
'--format' {
|
'--format' {
|
||||||
return @('json', 'mermaid', 'md', 'canvas') |
|
$formats = @('json', 'md', 'canvas')
|
||||||
|
if ($command -eq 'graph') {
|
||||||
|
$formats = @('json', 'mermaid', 'md', 'canvas')
|
||||||
|
}
|
||||||
|
return $formats |
|
||||||
Where-Object { $_ -like "$wordToComplete*" } |
|
Where-Object { $_ -like "$wordToComplete*" } |
|
||||||
ForEach-Object { New-GspCompletion $_ }
|
ForEach-Object { New-GspCompletion $_ }
|
||||||
}
|
}
|
||||||
@@ -87,6 +95,11 @@ Register-ArgumentCompleter -Native -CommandName gsp -ScriptBlock {
|
|||||||
Where-Object { $_ -like "$wordToComplete*" } |
|
Where-Object { $_ -like "$wordToComplete*" } |
|
||||||
ForEach-Object { New-GspCompletion $_ }
|
ForEach-Object { New-GspCompletion $_ }
|
||||||
}
|
}
|
||||||
|
'--for' {
|
||||||
|
return @('design', 'implement', 'review', 'test', 'acceptance', 'handoff', 'inspect') |
|
||||||
|
Where-Object { $_ -like "$wordToComplete*" } |
|
||||||
|
ForEach-Object { New-GspCompletion $_ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($command -eq 'completion') {
|
if ($command -eq 'completion') {
|
||||||
@@ -106,7 +119,13 @@ Register-ArgumentCompleter -Native -CommandName gsp -ScriptBlock {
|
|||||||
ForEach-Object { New-GspCompletion $_ }
|
ForEach-Object { New-GspCompletion $_ }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@('trace', 'flatten', 'pack', 'graph') -contains $command) {
|
if ($command -eq 'message') {
|
||||||
|
return @($script:GspFlags[$command]) |
|
||||||
|
Where-Object { $_ -like "$wordToComplete*" } |
|
||||||
|
ForEach-Object { New-GspCompletion $_ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@('trace', 'flatten', 'pack', '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 $_ }
|
||||||
@@ -125,11 +144,19 @@ _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 graph stage-check help"
|
local commands="init ai-init version completion validate index trace flatten pack impact message graph stage-check help"
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--format) COMPREPLY=( $(compgen -W "json mermaid md canvas" -- "$cur") ); return ;;
|
--format)
|
||||||
|
if [[ "$cmd" == "graph" ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W "json mermaid md canvas" -- "$cur") )
|
||||||
|
else
|
||||||
|
COMPREPLY=( $(compgen -W "json md canvas" -- "$cur") )
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
;;
|
||||||
--stage) COMPREPLY=( $(compgen -W "design integrate implement bind release" -- "$cur") ); return ;;
|
--stage) COMPREPLY=( $(compgen -W "design integrate implement bind release" -- "$cur") ); return ;;
|
||||||
--skill) COMPREPLY=( $(compgen -W "generic codex" -- "$cur") ); return ;;
|
--skill) COMPREPLY=( $(compgen -W "generic codex" -- "$cur") ); return ;;
|
||||||
|
--for) COMPREPLY=( $(compgen -W "design implement review test acceptance handoff inspect" -- "$cur") ); return ;;
|
||||||
esac
|
esac
|
||||||
if [[ $COMP_CWORD -eq 1 ]]; then
|
if [[ $COMP_CWORD -eq 1 ]]; then
|
||||||
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
||||||
@@ -143,7 +170,9 @@ _gsp_completion() {
|
|||||||
validate|index) COMPREPLY=( $(compgen -W "--root --out" -- "$cur") ) ;;
|
validate|index) COMPREPLY=( $(compgen -W "--root --out" -- "$cur") ) ;;
|
||||||
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 --depth --budget --include-type --exclude-type --out" -- "$cur") ) ;;
|
pack) COMPREPLY=( $(compgen -W "--root --for --stage --depth --budget --format --include-type --exclude-type --out" -- "$cur") ) ;;
|
||||||
|
impact) COMPREPLY=( $(compgen -W "--root --depth --format --out" -- "$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") ) ;;
|
||||||
stage-check) COMPREPLY=( $(compgen -W "--stage --root --out" -- "$cur") ) ;;
|
stage-check) COMPREPLY=( $(compgen -W "--stage --root --out" -- "$cur") ) ;;
|
||||||
completion) COMPREPLY=( $(compgen -W "powershell bash zsh fish install" -- "$cur") ) ;;
|
completion) COMPREPLY=( $(compgen -W "powershell bash zsh fish install" -- "$cur") ) ;;
|
||||||
@@ -151,7 +180,7 @@ _gsp_completion() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
trace|flatten|pack|graph)
|
trace|flatten|pack|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") )
|
||||||
@@ -177,6 +206,8 @@ _gsp() {
|
|||||||
'trace'
|
'trace'
|
||||||
'flatten'
|
'flatten'
|
||||||
'pack'
|
'pack'
|
||||||
|
'impact'
|
||||||
|
'message'
|
||||||
'graph'
|
'graph'
|
||||||
'stage-check'
|
'stage-check'
|
||||||
'help'
|
'help'
|
||||||
@@ -189,10 +220,12 @@ _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 graph stage-check help'
|
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 -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 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 trace flatten pack 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 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'')'
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ 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 "impact":
|
||||||
|
return runImpact(args[1:])
|
||||||
|
case "message":
|
||||||
|
return runMessage(args[1:])
|
||||||
case "graph":
|
case "graph":
|
||||||
return runGraph(args[1:])
|
return runGraph(args[1:])
|
||||||
case "stage-check":
|
case "stage-check":
|
||||||
@@ -73,7 +77,9 @@ Usage:
|
|||||||
gsp index [--root .] [--out index.json]
|
gsp index [--root .] [--out index.json]
|
||||||
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 .] [--for implement] [--stage implement] [--depth 3] [--budget 12000] [--format json|md|canvas] [--out context-pack.json]
|
||||||
|
gsp impact <id> [--root .] [--depth -1] [--format json|md|canvas] [--out impact.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]
|
||||||
gsp stage-check --stage implement [--root .] [--out stage-report.json]
|
gsp stage-check --stage implement [--root .] [--out stage-report.json]
|
||||||
`)
|
`)
|
||||||
@@ -320,6 +326,9 @@ func runPack(args []string) error {
|
|||||||
out := commonOut(fs)
|
out := commonOut(fs)
|
||||||
depth := fs.Int("depth", 3, "maximum relation depth; -1 means unlimited")
|
depth := fs.Int("depth", 3, "maximum relation depth; -1 means unlimited")
|
||||||
budget := fs.Int("budget", 0, "approximate JSON character budget; 0 means unlimited")
|
budget := fs.Int("budget", 0, "approximate JSON character budget; 0 means unlimited")
|
||||||
|
intent := fs.String("for", "", "context purpose: design, implement, review, test, acceptance, handoff, or inspect")
|
||||||
|
stage := fs.String("stage", "", "optional project stage")
|
||||||
|
format := fs.String("format", "json", "json, md, or canvas")
|
||||||
includeType := fs.String("include-type", "", "comma-separated type allow-list")
|
includeType := fs.String("include-type", "", "comma-separated type allow-list")
|
||||||
excludeType := fs.String("exclude-type", "", "comma-separated type deny-list")
|
excludeType := fs.String("exclude-type", "", "comma-separated type deny-list")
|
||||||
if err := fs.Parse(normalizeFlagArgs(args)); err != nil {
|
if err := fs.Parse(normalizeFlagArgs(args)); err != nil {
|
||||||
@@ -332,11 +341,91 @@ func runPack(args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
result := project.Pack(fs.Arg(0), *depth, *budget, gsp.Filter{
|
result := project.PackFor(fs.Arg(0), *intent, *stage, *depth, *budget, gsp.Filter{
|
||||||
IncludeTypes: splitCSV(*includeType),
|
IncludeTypes: splitCSV(*includeType),
|
||||||
ExcludeTypes: splitCSV(*excludeType),
|
ExcludeTypes: splitCSV(*excludeType),
|
||||||
})
|
})
|
||||||
|
switch *format {
|
||||||
|
case "json":
|
||||||
return writeJSON(*out, result)
|
return writeJSON(*out, result)
|
||||||
|
case "md":
|
||||||
|
return writeText(*out, result.Markdown())
|
||||||
|
case "canvas":
|
||||||
|
data, err := project.Graph(fs.Arg(0), *depth).Canvas()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeBytes(*out, data)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported pack format %q", *format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runImpact(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("impact", flag.ContinueOnError)
|
||||||
|
root := commonRoot(fs)
|
||||||
|
out := commonOut(fs)
|
||||||
|
depth := fs.Int("depth", -1, "maximum reverse relation depth; -1 means unlimited")
|
||||||
|
format := fs.String("format", "json", "json, md, or canvas")
|
||||||
|
if err := fs.Parse(normalizeFlagArgs(args)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fs.NArg() != 1 {
|
||||||
|
return fmt.Errorf("impact requires one GSP id")
|
||||||
|
}
|
||||||
|
project, err := gsp.LoadProject(*root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result := project.Impact(fs.Arg(0), *depth)
|
||||||
|
switch *format {
|
||||||
|
case "json":
|
||||||
|
return writeJSON(*out, result)
|
||||||
|
case "md":
|
||||||
|
return writeText(*out, result.Markdown())
|
||||||
|
case "canvas":
|
||||||
|
data, err := project.ImpactGraph(result).Canvas()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeBytes(*out, data)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported impact format %q", *format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMessage(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("message requires validate")
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "validate":
|
||||||
|
return runMessageValidate(args[1:])
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown message command %q", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMessageValidate(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("message validate", flag.ContinueOnError)
|
||||||
|
root := commonRoot(fs)
|
||||||
|
out := commonOut(fs)
|
||||||
|
if err := fs.Parse(normalizeFlagArgs(args)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fs.NArg() != 1 {
|
||||||
|
return fmt.Errorf("message validate requires one file")
|
||||||
|
}
|
||||||
|
project, err := gsp.LoadProject(*root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message, err := gsp.ReadMessage(project.Root, fs.Arg(0))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
report := project.ValidateMessage(message)
|
||||||
|
return writeReport(*out, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runGraph(args []string) error {
|
func runGraph(args []string) error {
|
||||||
@@ -418,6 +507,7 @@ func normalizeFlagArgs(args []string) []string {
|
|||||||
"-budget": true, "--budget": true,
|
"-budget": true, "--budget": true,
|
||||||
"-format": true, "--format": true,
|
"-format": true, "--format": true,
|
||||||
"-stage": true, "--stage": true,
|
"-stage": true, "--stage": true,
|
||||||
|
"-for": true, "--for": true,
|
||||||
"-name": true, "--name": true,
|
"-name": true, "--name": true,
|
||||||
"-entry": true, "--entry": true,
|
"-entry": true, "--entry": true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ 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 impact <id>`"+` before changing shared GSP units.
|
||||||
|
- 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.
|
||||||
|
|
||||||
`, gspVersion, scan)
|
`, gspVersion, scan)
|
||||||
|
|||||||
113
toolkit/internal/gsp/format.go
Normal file
113
toolkit/internal/gsp/format.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package gsp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p PackResult) Markdown() string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("# GSP Context Pack\n\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("- Entry: `%s`\n", p.Entry))
|
||||||
|
if p.Intent != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("- Intent: `%s`\n", p.Intent))
|
||||||
|
}
|
||||||
|
if p.Stage != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("- Stage: `%s`\n", p.Stage))
|
||||||
|
}
|
||||||
|
builder.WriteString(fmt.Sprintf("- Units: `%d`\n", p.Summary.UnitCount))
|
||||||
|
if p.Summary.MinResolution != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("- Resolution: `%s` to `%s`\n", p.Summary.MinResolution, p.Summary.MaxResolution))
|
||||||
|
}
|
||||||
|
if p.Budget > 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("- Budget: `%d`\n", p.Budget))
|
||||||
|
builder.WriteString(fmt.Sprintf("- Approx chars: `%d`\n", p.ApproxChars))
|
||||||
|
builder.WriteString(fmt.Sprintf("- Truncated: `%v`\n", p.Truncated))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n## Units\n\n")
|
||||||
|
for _, unit := range p.Units {
|
||||||
|
builder.WriteString(fmt.Sprintf("### %s\n\n", unit.DisplayTitle()))
|
||||||
|
builder.WriteString(fmt.Sprintf("- id: `%s`\n", unit.ID))
|
||||||
|
if unit.Type != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("- type: `%s`\n", unit.Type))
|
||||||
|
}
|
||||||
|
if unit.Resolution != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("- resolution: `%s`\n", unit.Resolution))
|
||||||
|
}
|
||||||
|
if unit.Refines != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("- refines: `%s`\n", unit.Refines))
|
||||||
|
}
|
||||||
|
if len(unit.With) > 0 {
|
||||||
|
var with []string
|
||||||
|
for _, rel := range unit.With {
|
||||||
|
with = append(with, "`"+rel.ID+"`")
|
||||||
|
}
|
||||||
|
sort.Strings(with)
|
||||||
|
builder.WriteString("- with: " + strings.Join(with, ", ") + "\n")
|
||||||
|
}
|
||||||
|
if unit.Context != "" {
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(unit.Context)
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
if len(p.Warnings) > 0 {
|
||||||
|
builder.WriteString("## Warnings\n\n")
|
||||||
|
for _, warning := range p.Warnings {
|
||||||
|
builder.WriteString(fmt.Sprintf("- `%s`: %s\n", warning.Code, warning.Message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ImpactResult) Markdown() string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("# GSP Impact\n\n")
|
||||||
|
builder.WriteString(fmt.Sprintf("- Entry: `%s`\n", r.Entry))
|
||||||
|
builder.WriteString(fmt.Sprintf("- Direct affected: `%d`\n", r.Summary.DirectCount))
|
||||||
|
builder.WriteString(fmt.Sprintf("- Indirect affected: `%d`\n", r.Summary.IndirectCount))
|
||||||
|
builder.WriteString(fmt.Sprintf("- Max depth: `%d`\n", r.Summary.MaxDepth))
|
||||||
|
writeImpactSection(&builder, "Direct", r.Direct)
|
||||||
|
writeImpactSection(&builder, "Indirect", r.Indirect)
|
||||||
|
if len(r.Warnings) > 0 {
|
||||||
|
builder.WriteString("## Warnings\n\n")
|
||||||
|
for _, warning := range r.Warnings {
|
||||||
|
builder.WriteString(fmt.Sprintf("- `%s`: %s\n", warning.Code, warning.Message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeImpactSection(builder *strings.Builder, title string, entries []ImpactEntry) {
|
||||||
|
builder.WriteString("\n## " + title + "\n\n")
|
||||||
|
if len(entries) == 0 {
|
||||||
|
builder.WriteString("None.\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
label := entry.Title
|
||||||
|
if label == "" {
|
||||||
|
label = entry.ID
|
||||||
|
}
|
||||||
|
builder.WriteString(fmt.Sprintf("- `%s` %s", entry.ID, label))
|
||||||
|
if entry.Kind != "" || entry.Via != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf(" via `%s` `%s`", entry.Kind, entry.Via))
|
||||||
|
}
|
||||||
|
if entry.Type != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf(" type `%s`", entry.Type))
|
||||||
|
}
|
||||||
|
if entry.Resolution != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf(" resolution `%s`", entry.Resolution))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Unit) DisplayTitle() string {
|
||||||
|
if strings.TrimSpace(u.Title) != "" {
|
||||||
|
return u.Title
|
||||||
|
}
|
||||||
|
return u.ID
|
||||||
|
}
|
||||||
@@ -96,6 +96,33 @@ func (g Graph) Markdown() string {
|
|||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Project) ImpactGraph(impact ImpactResult) Graph {
|
||||||
|
nodeMap := map[string]GraphNode{}
|
||||||
|
if unit, ok := p.ByID[impact.Entry]; ok {
|
||||||
|
nodeMap[impact.Entry] = GraphNode{ID: unit.ID, Title: unit.Title, Type: unit.Type, Resolution: unit.Resolution, File: unit.File}
|
||||||
|
} else {
|
||||||
|
nodeMap[impact.Entry] = GraphNode{ID: impact.Entry, Missing: true}
|
||||||
|
}
|
||||||
|
for _, entry := range append(append([]ImpactEntry{}, impact.Direct...), impact.Indirect...) {
|
||||||
|
nodeMap[entry.ID] = GraphNode{ID: entry.ID, Title: entry.Title, Type: entry.Type, Resolution: entry.Resolution, File: entry.File}
|
||||||
|
if _, ok := nodeMap[entry.Via]; !ok && entry.Via != "" {
|
||||||
|
if unit, exists := p.ByID[entry.Via]; exists {
|
||||||
|
nodeMap[entry.Via] = GraphNode{ID: unit.ID, Title: unit.Title, Type: unit.Type, Resolution: unit.Resolution, File: unit.File}
|
||||||
|
} else {
|
||||||
|
nodeMap[entry.Via] = GraphNode{ID: entry.Via, Missing: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes := make([]GraphNode, 0, len(nodeMap))
|
||||||
|
for _, node := range nodeMap {
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
}
|
||||||
|
sort.Slice(nodes, func(i, j int) bool {
|
||||||
|
return nodes[i].ID < nodes[j].ID
|
||||||
|
})
|
||||||
|
return Graph{Nodes: nodes, Edges: impact.Edges}
|
||||||
|
}
|
||||||
|
|
||||||
func (g Graph) Canvas() ([]byte, error) {
|
func (g Graph) Canvas() ([]byte, error) {
|
||||||
canvas := canvasDocument{
|
canvas := canvasDocument{
|
||||||
Nodes: make([]canvasNode, 0, len(g.Nodes)),
|
Nodes: make([]canvasNode, 0, len(g.Nodes)),
|
||||||
|
|||||||
99
toolkit/internal/gsp/message.go
Normal file
99
toolkit/internal/gsp/message.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package gsp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const GSPMessageVersion = "0.1"
|
||||||
|
|
||||||
|
var allowedMessageIntents = map[string]bool{
|
||||||
|
"design": true,
|
||||||
|
"implement": true,
|
||||||
|
"review": true,
|
||||||
|
"test": true,
|
||||||
|
"acceptance": true,
|
||||||
|
"handoff": true,
|
||||||
|
"inspect": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
MessageVersion string `json:"gspMessageVersion,omitempty" yaml:"gspMessageVersion"`
|
||||||
|
ID string `json:"id,omitempty" yaml:"id"`
|
||||||
|
From string `json:"from,omitempty" yaml:"from"`
|
||||||
|
To string `json:"to,omitempty" yaml:"to"`
|
||||||
|
Intent string `json:"intent,omitempty" yaml:"intent"`
|
||||||
|
Entry string `json:"entry,omitempty" yaml:"entry"`
|
||||||
|
Stage string `json:"stage,omitempty" yaml:"stage"`
|
||||||
|
Requires []string `json:"requires,omitempty" yaml:"requires"`
|
||||||
|
ContextPack ContextPack `json:"contextPack,omitempty" yaml:"contextPack"`
|
||||||
|
File string `json:"file,omitempty" yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextPack struct {
|
||||||
|
Mode string `json:"mode,omitempty" yaml:"mode"`
|
||||||
|
Depth int `json:"depth,omitempty" yaml:"depth"`
|
||||||
|
Budget int `json:"budget,omitempty" yaml:"budget"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadMessage(root, file string) (Message, error) {
|
||||||
|
path := file
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
path = filepath.Join(root, file)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
var message Message
|
||||||
|
if err := yaml.Unmarshal(data, &message); err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
message.File = relPath(root, path)
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) ValidateMessage(message Message) Report {
|
||||||
|
report := Report{OK: true}
|
||||||
|
if message.MessageVersion == "" {
|
||||||
|
report.addError("missing_message_version", message.ID, message.File, "message requires gspMessageVersion")
|
||||||
|
} else if message.MessageVersion != GSPMessageVersion {
|
||||||
|
report.addError("unsupported_message_version", message.ID, message.File, fmt.Sprintf("GSP message version %q is not supported", message.MessageVersion))
|
||||||
|
}
|
||||||
|
if message.ID == "" {
|
||||||
|
report.addError("missing_id", "", message.File, "message requires id")
|
||||||
|
}
|
||||||
|
if message.From == "" {
|
||||||
|
report.addError("missing_from", message.ID, message.File, "message requires from")
|
||||||
|
}
|
||||||
|
if message.To == "" {
|
||||||
|
report.addError("missing_to", message.ID, message.File, "message requires to")
|
||||||
|
}
|
||||||
|
if message.Intent == "" {
|
||||||
|
report.addError("missing_intent", message.ID, message.File, "message requires intent")
|
||||||
|
} else if !allowedMessageIntents[message.Intent] {
|
||||||
|
report.addError("invalid_intent", message.ID, message.File, fmt.Sprintf("intent %q is not allowed", message.Intent))
|
||||||
|
}
|
||||||
|
if message.Entry == "" {
|
||||||
|
report.addError("missing_entry", message.ID, message.File, "message requires entry")
|
||||||
|
} else if _, ok := p.ByID[message.Entry]; !ok {
|
||||||
|
report.addError("missing_entry_gsp", message.Entry, message.File, fmt.Sprintf("entry references missing GSP %q", message.Entry))
|
||||||
|
}
|
||||||
|
if message.Stage != "" {
|
||||||
|
if _, ok := p.Manifest.minResolution(message.Stage); !ok {
|
||||||
|
report.addError("unknown_stage", message.ID, message.File, fmt.Sprintf("unknown stage %q", message.Stage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, id := range message.Requires {
|
||||||
|
if _, ok := p.ByID[id]; !ok {
|
||||||
|
report.addError("missing_required_gsp", id, message.File, fmt.Sprintf("requires references missing GSP %q", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if message.ContextPack.Mode != "" && !allowedMessageIntents[message.ContextPack.Mode] {
|
||||||
|
report.addError("invalid_context_pack_mode", message.ID, message.File, fmt.Sprintf("contextPack mode %q is not allowed", message.ContextPack.Mode))
|
||||||
|
}
|
||||||
|
return report
|
||||||
|
}
|
||||||
@@ -131,14 +131,53 @@ type TraceResult struct {
|
|||||||
|
|
||||||
type PackResult struct {
|
type PackResult struct {
|
||||||
Entry string `json:"entry"`
|
Entry string `json:"entry"`
|
||||||
|
Intent string `json:"intent,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"`
|
||||||
ApproxChars int `json:"approxChars"`
|
ApproxChars int `json:"approxChars"`
|
||||||
Warnings []Issue `json:"warnings,omitempty"`
|
Warnings []Issue `json:"warnings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Summary struct {
|
||||||
|
UnitCount int `json:"unitCount"`
|
||||||
|
MinResolution string `json:"minResolution,omitempty"`
|
||||||
|
MaxResolution string `json:"maxResolution,omitempty"`
|
||||||
|
TypeCounts map[string]int `json:"typeCounts,omitempty"`
|
||||||
|
MissingCount int `json:"missingCount,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImpactResult struct {
|
||||||
|
Entry string `json:"entry"`
|
||||||
|
Depth int `json:"depth"`
|
||||||
|
Summary ImpactSummary `json:"summary"`
|
||||||
|
Direct []ImpactEntry `json:"direct,omitempty"`
|
||||||
|
Indirect []ImpactEntry `json:"indirect,omitempty"`
|
||||||
|
Edges []GraphEdge `json:"edges,omitempty"`
|
||||||
|
Warnings []Issue `json:"warnings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImpactSummary struct {
|
||||||
|
DirectCount int `json:"directCount"`
|
||||||
|
IndirectCount int `json:"indirectCount"`
|
||||||
|
TotalCount int `json:"totalCount"`
|
||||||
|
MaxDepth int `json:"maxDepth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImpactEntry struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Resolution string `json:"resolution,omitempty"`
|
||||||
|
File string `json:"file,omitempty"`
|
||||||
|
Depth int `json:"depth"`
|
||||||
|
Via string `json:"via,omitempty"`
|
||||||
|
Kind string `json:"kind,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Graph struct {
|
type Graph struct {
|
||||||
Nodes []GraphNode `json:"nodes"`
|
Nodes []GraphNode `json:"nodes"`
|
||||||
Edges []GraphEdge `json:"edges"`
|
Edges []GraphEdge `json:"edges"`
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Project) Validate(stage string) Report {
|
func (p *Project) Validate(stage string) Report {
|
||||||
@@ -165,6 +166,10 @@ func (p *Project) Flatten(id string, depth int, filter Filter) FlattenResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Project) Pack(id string, depth, budget int, filter Filter) PackResult {
|
func (p *Project) Pack(id string, depth, budget int, filter Filter) PackResult {
|
||||||
|
return p.PackFor(id, "", "", depth, budget, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) PackFor(id, intent, stage string, depth, budget int, filter Filter) PackResult {
|
||||||
flattened := p.Flatten(id, depth, filter)
|
flattened := p.Flatten(id, depth, filter)
|
||||||
units := make([]*Unit, 0, len(flattened.Units))
|
units := make([]*Unit, 0, len(flattened.Units))
|
||||||
approx := 0
|
approx := 0
|
||||||
@@ -181,15 +186,131 @@ func (p *Project) Pack(id string, depth, budget int, filter Filter) PackResult {
|
|||||||
}
|
}
|
||||||
return PackResult{
|
return PackResult{
|
||||||
Entry: id,
|
Entry: id,
|
||||||
|
Intent: intent,
|
||||||
|
Stage: stage,
|
||||||
Depth: depth,
|
Depth: depth,
|
||||||
Budget: budget,
|
Budget: budget,
|
||||||
Truncated: truncated,
|
Truncated: truncated,
|
||||||
Units: units,
|
Units: units,
|
||||||
|
Summary: summarizeUnits(units, len(flattened.Warnings)),
|
||||||
ApproxChars: approx,
|
ApproxChars: approx,
|
||||||
Warnings: flattened.Warnings,
|
Warnings: flattened.Warnings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func summarizeUnits(units []*Unit, missingCount int) Summary {
|
||||||
|
summary := Summary{
|
||||||
|
UnitCount: len(units),
|
||||||
|
TypeCounts: map[string]int{},
|
||||||
|
MissingCount: missingCount,
|
||||||
|
}
|
||||||
|
min := 99
|
||||||
|
max := -1
|
||||||
|
for _, unit := range units {
|
||||||
|
if unit.Type != "" {
|
||||||
|
summary.TypeCounts[unit.Type]++
|
||||||
|
}
|
||||||
|
rank, ok := resolutionValue(unit.Resolution)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rank < min {
|
||||||
|
min = rank
|
||||||
|
summary.MinResolution = displayResolution(unit.Resolution)
|
||||||
|
}
|
||||||
|
if rank > max {
|
||||||
|
max = rank
|
||||||
|
summary.MaxResolution = displayResolution(unit.Resolution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(summary.TypeCounts) == 0 {
|
||||||
|
summary.TypeCounts = nil
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) Impact(id string, depth int) ImpactResult {
|
||||||
|
result := ImpactResult{Entry: id, Depth: depth}
|
||||||
|
if _, ok := p.ByID[id]; !ok {
|
||||||
|
result.Warnings = append(result.Warnings, Issue{Level: "warning", Code: "missing", ID: id, Message: fmt.Sprintf("missing GSP %q", id)})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse := map[string][]ImpactEntry{}
|
||||||
|
for _, unit := range p.Units {
|
||||||
|
if unit.Refines != "" {
|
||||||
|
reverse[unit.Refines] = append(reverse[unit.Refines], impactEntry(unit, 0, unit.Refines, "refines"))
|
||||||
|
}
|
||||||
|
for _, rel := range unit.With {
|
||||||
|
reverse[rel.ID] = append(reverse[rel.ID], impactEntry(unit, 0, rel.ID, "with"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range reverse {
|
||||||
|
sort.Slice(reverse[key], func(i, j int) bool {
|
||||||
|
return reverse[key][i].ID < reverse[key][j].ID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := map[string]bool{id: true}
|
||||||
|
queue := []ImpactEntry{{ID: id, Depth: 0}}
|
||||||
|
for len(queue) > 0 {
|
||||||
|
current := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
if depth >= 0 && current.Depth >= depth {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, affected := range reverse[current.ID] {
|
||||||
|
if seen[affected.ID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[affected.ID] = true
|
||||||
|
affected.Depth = current.Depth + 1
|
||||||
|
affected.Via = current.ID
|
||||||
|
if affected.Depth == 1 {
|
||||||
|
result.Direct = append(result.Direct, affected)
|
||||||
|
} else {
|
||||||
|
result.Indirect = append(result.Indirect, affected)
|
||||||
|
}
|
||||||
|
result.Edges = append(result.Edges, GraphEdge{From: affected.ID, To: current.ID, Kind: affected.Kind})
|
||||||
|
queue = append(queue, affected)
|
||||||
|
if affected.Depth > result.Summary.MaxDepth {
|
||||||
|
result.Summary.MaxDepth = affected.Depth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sortImpactEntries(result.Direct)
|
||||||
|
sortImpactEntries(result.Indirect)
|
||||||
|
sort.Slice(result.Edges, func(i, j int) bool {
|
||||||
|
return strings.Join([]string{result.Edges[i].From, result.Edges[i].To, result.Edges[i].Kind}, "\x00") < strings.Join([]string{result.Edges[j].From, result.Edges[j].To, result.Edges[j].Kind}, "\x00")
|
||||||
|
})
|
||||||
|
result.Summary.DirectCount = len(result.Direct)
|
||||||
|
result.Summary.IndirectCount = len(result.Indirect)
|
||||||
|
result.Summary.TotalCount = len(result.Direct) + len(result.Indirect)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func impactEntry(unit *Unit, depth int, via, kind string) ImpactEntry {
|
||||||
|
return ImpactEntry{
|
||||||
|
ID: unit.ID,
|
||||||
|
Title: unit.Title,
|
||||||
|
Type: unit.Type,
|
||||||
|
Resolution: unit.Resolution,
|
||||||
|
File: unit.File,
|
||||||
|
Depth: depth,
|
||||||
|
Via: via,
|
||||||
|
Kind: kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortImpactEntries(entries []ImpactEntry) {
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
if entries[i].Depth == entries[j].Depth {
|
||||||
|
return entries[i].ID < entries[j].ID
|
||||||
|
}
|
||||||
|
return entries[i].Depth < entries[j].Depth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type walker struct {
|
type walker struct {
|
||||||
project *Project
|
project *Project
|
||||||
depth int
|
depth int
|
||||||
|
|||||||
@@ -91,6 +91,25 @@ context: Base lottery page.
|
|||||||
if !foundTitle {
|
if !foundTitle {
|
||||||
t.Fatalf("expected canvas node text to include title, got %+v", canvas.Nodes)
|
t.Fatalf("expected canvas node text to include title, got %+v", canvas.Nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pack := project.PackFor("page.lottery.main", "implement", "implement", -1, 0, Filter{})
|
||||||
|
if pack.Summary.UnitCount != 4 {
|
||||||
|
t.Fatalf("pack unit count = %d", pack.Summary.UnitCount)
|
||||||
|
}
|
||||||
|
if !strings.Contains(pack.Markdown(), "GSP Context Pack") {
|
||||||
|
t.Fatal("expected pack markdown")
|
||||||
|
}
|
||||||
|
|
||||||
|
impact := project.Impact("feedback.positive", -1)
|
||||||
|
if impact.Summary.DirectCount != 1 {
|
||||||
|
t.Fatalf("impact direct count = %d", impact.Summary.DirectCount)
|
||||||
|
}
|
||||||
|
if impact.Direct[0].ID != "page.lottery.main" {
|
||||||
|
t.Fatalf("impact direct = %+v", impact.Direct)
|
||||||
|
}
|
||||||
|
if !strings.Contains(impact.Markdown(), "GSP Impact") {
|
||||||
|
t.Fatal("expected impact markdown")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateMissingReference(t *testing.T) {
|
func TestValidateMissingReference(t *testing.T) {
|
||||||
@@ -263,6 +282,40 @@ func TestInitAIUsageRequiresManifest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateMessage(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
if _, err := InitProject(root, InitOptions{
|
||||||
|
Entry: "page.sample.main",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeTestFile(t, root, "message.gspmsg", `gspMessageVersion: 0.1
|
||||||
|
id: msg.sample
|
||||||
|
from: planner
|
||||||
|
to: implementer
|
||||||
|
intent: implement
|
||||||
|
entry: page.sample.main
|
||||||
|
stage: implement
|
||||||
|
requires:
|
||||||
|
- page.sample.main
|
||||||
|
contextPack:
|
||||||
|
mode: implement
|
||||||
|
depth: -1
|
||||||
|
`)
|
||||||
|
project, err := LoadProject(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
message, err := ReadMessage(project.Root, "message.gspmsg")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
report := project.ValidateMessage(message)
|
||||||
|
if !report.OK {
|
||||||
|
t.Fatalf("expected valid message, got %+v", report.Errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeTestFile(t *testing.T, root, name, content string) {
|
func writeTestFile(t *testing.T, root, name, content string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
path := filepath.Join(root, name)
|
path := filepath.Join(root, name)
|
||||||
|
|||||||
Reference in New Issue
Block a user