Skip to content

Commit bada264

Browse files
committed
refactor check domain model
1 parent 8e0161f commit bada264

46 files changed

Lines changed: 1486 additions & 1488 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/helix/01-frame/feature-registry.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ This registry tracks the core Dun features and their current status.
2424
| F-019 | Installer + self-updater | P1 | Planned | One-line install + `dun update` |
2525
| F-020 | Generic command checks | P1 | Planned | Command checks + external plugin loading |
2626
| F-021 | Beads work routing | P1 | Planned | Surface ready Beads tasks in prompts |
27+
| F-023 | Check domain model | P1 | In progress | Registry-based check lifecycle, summaries, scoring |
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
dun:
3+
id: F-023
4+
depends_on:
5+
- helix.prd
6+
review:
7+
self_hash: 9b5f839c24e68e270c4166652ec89c5334406b462deefe97ac09582d8ae6263e
8+
deps:
9+
helix.prd: 07d49919dec51a33254b7630622ee086a5108ed5deecd456f7228f03712e699d
10+
---
11+
# Feature Spec: F-023 Check Domain Model
12+
13+
## Summary
14+
15+
Make checks a first-class domain model with a consistent lifecycle (discover,
16+
plan, evaluate, summarize, prompt, score) so Dun can extend and reason about
17+
checks without special-case code.
18+
19+
## Requirements
20+
21+
- Represent checks as explicit domain objects with shared metadata and
22+
type-specific configuration.
23+
- Provide a registry for check types so execution is data-driven (no monolithic
24+
switch statements).
25+
- Standardize check results with summary, score, prompt metadata, and optional
26+
update/freshness signals.
27+
- Preserve existing CLI behavior and check outputs while improving internal
28+
structure.
29+
- Ensure prompt generation and task summaries remain bounded and deterministic.
30+
31+
## Acceptance Criteria
32+
33+
- Check planning uses a typed registry to decode and run checks.
34+
- Check results include a summary and score field (optional in output when
35+
unset).
36+
- Check types that detect stale/missing artifacts surface update signals in a
37+
consistent shape.
38+
- Existing checks and plugins continue to run without changes to plugin YAML.
39+
- `go test ./...` passes.
40+
41+
## Gaps & Conflicts
42+
43+
- None identified.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
dun:
3+
id: US-023
4+
depends_on:
5+
- F-023
6+
review:
7+
self_hash: 68b0c2dbf86881914c5acbd5cbe3eefc20eee60ec8e0d3539b97a8ac11a53cfa
8+
deps:
9+
F-023: 9b5f839c24e68e270c4166652ec89c5334406b462deefe97ac09582d8ae6263e
10+
---
11+
# US-023: First-Class Check Domain Model
12+
13+
As a maintainer, I want checks to be modeled as first-class domain objects so
14+
Dun can discover, evaluate, summarize, and score them consistently without
15+
special-case logic.
16+
17+
## Acceptance Criteria
18+
19+
- Check definitions are normalized into a common domain structure.
20+
- Check execution is routed through a check type registry.
21+
- Check results include summary and score fields.
22+
- Update/freshness signals are available for checks that detect staleness.
23+
- Existing plugin manifests remain compatible.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
dun:
3+
id: SD-023
4+
depends_on:
5+
- F-023
6+
review:
7+
self_hash: 08154ce72da29c20073be694be803ec654134d755e8b877ac69b03d8edd59dd2
8+
deps:
9+
F-023: 9b5f839c24e68e270c4166652ec89c5334406b462deefe97ac09582d8ae6263e
10+
---
11+
# Solution Design: Check Domain Model
12+
13+
## Problem
14+
15+
Check behavior is implemented through a monolithic config structure and a large
16+
switch statement, making new check types and consistent summarization difficult.
17+
18+
## Goals
19+
20+
- Introduce a clear check lifecycle: discover, plan, evaluate, summarize, prompt,
21+
score.
22+
- Decouple check execution from the engine using a registry of check types.
23+
- Standardize result metadata without breaking existing outputs.
24+
25+
## Approach
26+
27+
- Keep plugin YAMLs intact, but normalize check definitions into a common
28+
structure and decode type-specific configs via a registry.
29+
- Add a summarizer that sets `summary`, `score`, and optional update signals on
30+
check results after evaluation.
31+
- Keep CLI rendering as a thin adapter over structured results.
32+
33+
## Components
34+
35+
- Check Registry: maps check types to type handlers.
36+
- Check Definition: normalized metadata shared across types.
37+
- Type Configs: per-check-type config structs.
38+
- Summarizer: derives summary/score/update signals from results.
39+
40+
## Open Questions
41+
42+
- None beyond technical design details.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
dun:
3+
id: TD-023
4+
depends_on:
5+
- US-023
6+
review:
7+
self_hash: 9522804907a7c19f06e7868543b17c4fb35218c841953b07a058ac5d8360f6c4
8+
deps:
9+
US-023: 68b0c2dbf86881914c5acbd5cbe3eefc20eee60ec8e0d3539b97a8ac11a53cfa
10+
---
11+
# TD-023: Check Domain Model + Registry
12+
13+
## Overview
14+
15+
Implement a registry-based check pipeline with typed configs and a consistent
16+
result schema that includes summary, score, and update signals.
17+
18+
## Data Model
19+
20+
- `CheckSpec`: raw manifest definition (YAML) used for decoding and plan
21+
inspection.
22+
- `CheckDefinition`: normalized metadata (id, description, phase, priority,
23+
conditions, plugin id).
24+
- `CheckConfig`: type-specific config struct decoded from `CheckSpec`.
25+
- `CheckResult`: status/signal/detail/issues plus optional `summary`, `score`,
26+
and `update`.
27+
28+
## Pipeline
29+
30+
1. Load plugins and build plan from `CheckSpec`.
31+
2. Decode each `CheckSpec` using the registry into a typed config.
32+
3. Execute the check handler with `CheckDefinition`, config, and options.
33+
4. Post-process results with a summarizer (summary + score + update signals).
34+
35+
## Registry Interface
36+
37+
- `Type() string`
38+
- `Decode(CheckSpec) (CheckConfig, error)`
39+
- `Run(root, def, config, opts) (CheckResult, error)`
40+
41+
Optional: `Explain(config) CheckExplain` for `dun explain` details.
42+
43+
## Summarization
44+
45+
- Summary: short, deterministic string derived from status + signal + detail.
46+
- Score: numeric score derived from status (pass > warn > fail/skip).
47+
- Update: optional structured signal for checks that detect staleness.
48+
49+
## Backwards Compatibility
50+
51+
- Plugin YAML structure stays unchanged.
52+
- CLI outputs remain compatible; new fields are additive.
53+
54+
## Testing
55+
56+
- Registry dispatch tests for each check type.
57+
- Config decode tests for representative check types.
58+
- Summary/score tests for status variants.
59+
- Full `go test ./...` pass.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
dun:
3+
id: TP-023
4+
depends_on:
5+
- TD-023
6+
review:
7+
self_hash: cc509d37fd92febd897f7565e61a9da76ecba61c834f57c54c3e9e41fb8fbc26
8+
deps:
9+
TD-023: 9522804907a7c19f06e7868543b17c4fb35218c841953b07a058ac5d8360f6c4
10+
---
11+
# TP-023: Check Domain Model
12+
13+
## Scope
14+
15+
Verify registry-based execution, typed config decoding, summary/score fields,
16+
update signals, and backward compatibility.
17+
18+
## Acceptance Criteria
19+
20+
| ID | Criteria |
21+
|----|----------|
22+
| AC-1 | Registry dispatch runs all existing check types |
23+
| AC-2 | Typed config decode succeeds for representative check types |
24+
| AC-3 | Summary/score fields are populated and deterministic |
25+
| AC-4 | Update signals appear for stale/missing checks |
26+
| AC-5 | `go test ./...` passes |
27+
28+
## Proposed Tests
29+
30+
- Registry dispatch unit tests per check type.
31+
- Summarizer tests for each status value.
32+
- Golden test for `dun check --format=json` to ensure additive fields only.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
dun:
3+
id: IP-023
4+
depends_on:
5+
- TD-023
6+
review:
7+
self_hash: 4b2a6fc43b11b206e13e208ea61a36f373a9ef8a2c3b6967e676864333c03ee1
8+
deps:
9+
TD-023: 9522804907a7c19f06e7868543b17c4fb35218c841953b07a058ac5d8360f6c4
10+
---
11+
# IP-023: Implement Check Domain Model
12+
13+
## Steps
14+
15+
1. Introduce `CheckDefinition`, `CheckSpec`, and a `CheckType` registry.
16+
2. Update engine planning/execution to use the registry.
17+
3. Create typed config structs for each check type and decode from `CheckSpec`.
18+
4. Add summary/score/update fields to `CheckResult` and implement summarizer.
19+
5. Update CLI rendering to prefer summaries and preserve existing output.
20+
6. Update tests and run `go test ./...`.
21+
22+
## Acceptance
23+
24+
- All existing checks execute with no behavior regressions.
25+
- Summary/score fields are present and deterministic.
26+
- Tests pass.

internal/dun/agent.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type PromptContext struct {
4242
AutomationMode string
4343
}
4444

45-
func runAgentCheck(root string, plugin Plugin, check Check, opts Options) (CheckResult, error) {
45+
func runAgentCheck(root string, plugin Plugin, def CheckDefinition, config AgentCheckConfig, opts Options) (CheckResult, error) {
4646
mode, err := normalizeAgentMode(opts.AgentMode)
4747
if err != nil {
4848
return CheckResult{}, err
@@ -52,21 +52,21 @@ func runAgentCheck(root string, plugin Plugin, check Check, opts Options) (Check
5252
return CheckResult{}, err
5353
}
5454

55-
envelope, err := buildPromptEnvelope(root, plugin, check, automation)
55+
envelope, err := buildPromptEnvelope(root, plugin, def, config, automation)
5656
if err != nil {
5757
return CheckResult{}, err
5858
}
5959

6060
if mode != "auto" {
61-
return promptResult(check, envelope, "agent prompt ready", check.Description), nil
61+
return promptResult(def, envelope, "agent prompt ready", def.Description), nil
6262
}
6363

6464
agentCmd := opts.AgentCmd
6565
if agentCmd == "" {
6666
agentCmd = os.Getenv("DUN_AGENT_CMD")
6767
}
6868
if agentCmd == "" {
69-
return promptResult(check, envelope, "agent not configured", "set --agent-cmd or DUN_AGENT_CMD to run in auto mode"), nil
69+
return promptResult(def, envelope, "agent not configured", "set --agent-cmd or DUN_AGENT_CMD to run in auto mode"), nil
7070
}
7171

7272
timeout := opts.AgentTimeout
@@ -84,7 +84,7 @@ func runAgentCheck(root string, plugin Plugin, check Check, opts Options) (Check
8484
}
8585

8686
return CheckResult{
87-
ID: check.ID,
87+
ID: def.ID,
8888
Status: resp.Status,
8989
Signal: resp.Signal,
9090
Detail: resp.Detail,
@@ -117,13 +117,13 @@ func normalizeAutomationMode(mode string) (string, error) {
117117
}
118118
}
119119

120-
func promptResult(check Check, envelope PromptEnvelope, signal string, detail string) CheckResult {
120+
func promptResult(def CheckDefinition, envelope PromptEnvelope, signal string, detail string) CheckResult {
121121
next := envelope.Callback.Command
122122
if next == "" {
123-
next = fmt.Sprintf("dun respond --id %s --response -", check.ID)
123+
next = fmt.Sprintf("dun respond --id %s --response -", def.ID)
124124
}
125125
return CheckResult{
126-
ID: check.ID,
126+
ID: def.ID,
127127
Status: "prompt",
128128
Signal: signal,
129129
Detail: detail,
@@ -132,13 +132,13 @@ func promptResult(check Check, envelope PromptEnvelope, signal string, detail st
132132
}
133133
}
134134

135-
func buildPromptEnvelope(root string, plugin Plugin, check Check, automationMode string) (PromptEnvelope, error) {
136-
inputs, err := resolveInputs(root, check.Inputs)
135+
func buildPromptEnvelope(root string, plugin Plugin, def CheckDefinition, config AgentCheckConfig, automationMode string) (PromptEnvelope, error) {
136+
inputs, err := resolveInputs(root, config.Inputs)
137137
if err != nil {
138138
return PromptEnvelope{}, err
139139
}
140140

141-
promptText, schemaText, err := renderPromptText(plugin, check, inputs, automationMode)
141+
promptText, schemaText, err := renderPromptText(plugin, config, def.ID, inputs, automationMode)
142142
if err != nil {
143143
return PromptEnvelope{}, err
144144
}
@@ -150,21 +150,21 @@ func buildPromptEnvelope(root string, plugin Plugin, check Check, automationMode
150150

151151
return PromptEnvelope{
152152
Kind: "dun.prompt.v1",
153-
ID: check.ID,
154-
Title: check.Description,
155-
Summary: check.Description,
153+
ID: def.ID,
154+
Title: def.Description,
155+
Summary: def.Description,
156156
Prompt: promptText,
157157
Inputs: inputPaths,
158158
ResponseSchema: schemaText,
159159
Callback: PromptCallback{
160-
Command: fmt.Sprintf("dun respond --id %s --response -", check.ID),
160+
Command: fmt.Sprintf("dun respond --id %s --response -", def.ID),
161161
Stdin: true,
162162
},
163163
}, nil
164164
}
165165

166-
func renderPromptText(plugin Plugin, check Check, inputs []PromptInput, automationMode string) (string, string, error) {
167-
tmplText, err := loadPromptTemplate(plugin, check.Prompt)
166+
func renderPromptText(plugin Plugin, config AgentCheckConfig, checkID string, inputs []PromptInput, automationMode string) (string, string, error) {
167+
tmplText, err := loadPromptTemplate(plugin, config.Prompt)
168168
if err != nil {
169169
return "", "", err
170170
}
@@ -176,7 +176,7 @@ func renderPromptText(plugin Plugin, check Check, inputs []PromptInput, automati
176176

177177
var buf bytes.Buffer
178178
ctx := PromptContext{
179-
CheckID: check.ID,
179+
CheckID: checkID,
180180
Inputs: inputs,
181181
AutomationMode: automationMode,
182182
}
@@ -185,8 +185,8 @@ func renderPromptText(plugin Plugin, check Check, inputs []PromptInput, automati
185185
}
186186

187187
var schemaText string
188-
if check.ResponseSchema != "" {
189-
loaded, err := loadPromptTemplate(plugin, check.ResponseSchema)
188+
if config.ResponseSchema != "" {
189+
loaded, err := loadPromptTemplate(plugin, config.ResponseSchema)
190190
if err != nil {
191191
return "", "", err
192192
}

0 commit comments

Comments
 (0)