@dannylee1020/kkt 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +275 -0
- package/assets/kkt-readme-modern.png +0 -0
- package/bin/kkt-install.mjs +25 -0
- package/cmd/kkt/main.go +15 -0
- package/go.mod +3 -0
- package/internal/workflow/cli.go +349 -0
- package/internal/workflow/cli_test.go +1266 -0
- package/internal/workflow/guardrails.go +900 -0
- package/internal/workflow/init.go +164 -0
- package/internal/workflow/init_test.go +113 -0
- package/internal/workflow/operations.go +1855 -0
- package/internal/workflow/validation_commands.go +291 -0
- package/internal/workflow/workspace.go +802 -0
- package/internal/workflow/workspace_test.go +285 -0
- package/package.json +45 -0
- package/scripts/install-cli.sh +210 -0
- package/scripts/install.sh +644 -0
- package/skills/kkt/SKILL.md +74 -0
- package/skills/kkt/references/discovery-tooling.md +53 -0
- package/skills/kkt/references/feature-optimization-model.md +120 -0
- package/skills/kkt/references/kkt-kernel.md +58 -0
- package/skills/kkt/references/layered-modeling-methods.md +101 -0
- package/skills/kkt/references/plan-assimilation.md +46 -0
- package/skills/kkt/references/schemas.md +231 -0
- package/skills/kkt/references/state-contract.md +76 -0
- package/skills/kkt-loop/SKILL.md +99 -0
- package/skills/kkt-loop/references/discovery-tooling.md +53 -0
- package/skills/kkt-loop/references/feature-optimization-model.md +120 -0
- package/skills/kkt-loop/references/kkt-kernel.md +58 -0
- package/skills/kkt-loop/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-loop/references/plan-assimilation.md +46 -0
- package/skills/kkt-loop/references/schemas.md +231 -0
- package/skills/kkt-loop/references/state-contract.md +76 -0
- package/skills/kkt-model/SKILL.md +76 -0
- package/skills/kkt-model/references/discovery-tooling.md +53 -0
- package/skills/kkt-model/references/feature-optimization-model.md +120 -0
- package/skills/kkt-model/references/kkt-kernel.md +58 -0
- package/skills/kkt-model/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-model/references/plan-assimilation.md +46 -0
- package/skills/kkt-model/references/schemas.md +231 -0
- package/skills/kkt-model/references/state-contract.md +76 -0
- package/skills/kkt-run/SKILL.md +62 -0
- package/skills/kkt-run/references/discovery-tooling.md +53 -0
- package/skills/kkt-run/references/feature-optimization-model.md +120 -0
- package/skills/kkt-run/references/kkt-kernel.md +58 -0
- package/skills/kkt-run/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-run/references/plan-assimilation.md +46 -0
- package/skills/kkt-run/references/schemas.md +231 -0
- package/skills/kkt-run/references/state-contract.md +76 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
package workflow
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"errors"
|
|
9
|
+
"fmt"
|
|
10
|
+
"io"
|
|
11
|
+
"os"
|
|
12
|
+
"os/exec"
|
|
13
|
+
"path/filepath"
|
|
14
|
+
"sort"
|
|
15
|
+
"strings"
|
|
16
|
+
"time"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
type validationCommandProof struct {
|
|
20
|
+
Command string
|
|
21
|
+
Status string
|
|
22
|
+
ExitCode int
|
|
23
|
+
DurationMS int64
|
|
24
|
+
Timestamp string
|
|
25
|
+
Log string
|
|
26
|
+
Fingerprint string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func RunRequiredValidationCommands(root, workspace string, timeout time.Duration, stdout io.Writer) error {
|
|
30
|
+
commands, err := requiredValidationCommands(workspace)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return err
|
|
33
|
+
}
|
|
34
|
+
if len(commands) == 0 {
|
|
35
|
+
fmt.Fprintln(stdout, "no required validation commands")
|
|
36
|
+
return nil
|
|
37
|
+
}
|
|
38
|
+
projectRootDir, err := projectRootForWorkspace(root, workspace)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return err
|
|
41
|
+
}
|
|
42
|
+
if err := os.MkdirAll(filepath.Join(workspace, "validation"), 0o755); err != nil {
|
|
43
|
+
return err
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var failed []string
|
|
47
|
+
for _, command := range commands {
|
|
48
|
+
proof, runErr := runValidationCommand(projectRootDir, workspace, command, timeout)
|
|
49
|
+
if proof.Command == "" {
|
|
50
|
+
return runErr
|
|
51
|
+
}
|
|
52
|
+
eventErr := appendValidationCommandEvent(workspace, proof)
|
|
53
|
+
evidenceErr := appendValidationCommandEvidence(workspace, proof)
|
|
54
|
+
fmt.Fprintf(stdout, "%s: %s\n", proof.Status, command)
|
|
55
|
+
fmt.Fprintf(stdout, "log: %s\n", proof.Log)
|
|
56
|
+
if runErr != nil {
|
|
57
|
+
failed = append(failed, command)
|
|
58
|
+
}
|
|
59
|
+
if eventErr != nil {
|
|
60
|
+
return eventErr
|
|
61
|
+
}
|
|
62
|
+
if evidenceErr != nil {
|
|
63
|
+
return evidenceErr
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if len(failed) > 0 {
|
|
68
|
+
return fmt.Errorf("validation command failed: %s", strings.Join(failed, ", "))
|
|
69
|
+
}
|
|
70
|
+
return markEvidenceRecorded(workspace)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func runValidationCommand(projectRootDir, workspace, command string, timeout time.Duration) (validationCommandProof, error) {
|
|
74
|
+
start := time.Now().UTC()
|
|
75
|
+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
76
|
+
defer cancel()
|
|
77
|
+
|
|
78
|
+
var output bytes.Buffer
|
|
79
|
+
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
|
80
|
+
cmd.Dir = projectRootDir
|
|
81
|
+
cmd.Stdout = &output
|
|
82
|
+
cmd.Stderr = &output
|
|
83
|
+
runErr := cmd.Run()
|
|
84
|
+
|
|
85
|
+
exitCode := 0
|
|
86
|
+
status := "passed"
|
|
87
|
+
if runErr != nil {
|
|
88
|
+
status = "failed"
|
|
89
|
+
exitCode = -1
|
|
90
|
+
var exitErr *exec.ExitError
|
|
91
|
+
if errors.As(runErr, &exitErr) {
|
|
92
|
+
exitCode = exitErr.ExitCode()
|
|
93
|
+
}
|
|
94
|
+
if ctx.Err() == context.DeadlineExceeded {
|
|
95
|
+
output.WriteString(fmt.Sprintf("\ncommand timed out after %s\n", timeout))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
timestamp := start.Format("20060102-150405")
|
|
100
|
+
logPath := filepath.Join(workspace, "validation", fmt.Sprintf("%s-%s.log", timestamp, slugify(command)))
|
|
101
|
+
if err := os.WriteFile(logPath, output.Bytes(), 0o644); err != nil {
|
|
102
|
+
return validationCommandProof{}, err
|
|
103
|
+
}
|
|
104
|
+
fingerprint, err := workingTreeFingerprint(projectRootDir, workspace)
|
|
105
|
+
if err != nil {
|
|
106
|
+
return validationCommandProof{}, err
|
|
107
|
+
}
|
|
108
|
+
proof := validationCommandProof{
|
|
109
|
+
Command: command,
|
|
110
|
+
Status: status,
|
|
111
|
+
ExitCode: exitCode,
|
|
112
|
+
DurationMS: time.Since(start).Milliseconds(),
|
|
113
|
+
Timestamp: start.Format(time.RFC3339),
|
|
114
|
+
Log: logPath,
|
|
115
|
+
Fingerprint: fingerprint,
|
|
116
|
+
}
|
|
117
|
+
return proof, runErr
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func requiredValidationCommands(workspace string) ([]string, error) {
|
|
121
|
+
contract, err := readGuardrails(workspace)
|
|
122
|
+
if err != nil {
|
|
123
|
+
if os.IsNotExist(err) {
|
|
124
|
+
return nil, nil
|
|
125
|
+
}
|
|
126
|
+
return nil, err
|
|
127
|
+
}
|
|
128
|
+
return uniqueNonEmpty(contract.Validation.RequiredCommands), nil
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func validationCommandProofIssues(workspace string) []string {
|
|
132
|
+
commands, err := requiredValidationCommands(workspace)
|
|
133
|
+
if err != nil {
|
|
134
|
+
return []string{"guardrails.json could not be read: " + err.Error()}
|
|
135
|
+
}
|
|
136
|
+
if len(commands) == 0 {
|
|
137
|
+
return nil
|
|
138
|
+
}
|
|
139
|
+
projectRootDir, err := projectRootForWorkspace(".", workspace)
|
|
140
|
+
if err != nil {
|
|
141
|
+
return []string{"could not resolve project root for validation proof: " + err.Error()}
|
|
142
|
+
}
|
|
143
|
+
fingerprint, err := workingTreeFingerprint(projectRootDir, workspace)
|
|
144
|
+
if err != nil {
|
|
145
|
+
return []string{"could not fingerprint working tree for validation proof: " + err.Error()}
|
|
146
|
+
}
|
|
147
|
+
proofs, err := latestValidationCommandProofs(workspace)
|
|
148
|
+
if err != nil {
|
|
149
|
+
return []string{"could not read validation command proof: " + err.Error()}
|
|
150
|
+
}
|
|
151
|
+
var issues []string
|
|
152
|
+
for _, command := range commands {
|
|
153
|
+
proof, ok := proofs[command]
|
|
154
|
+
if !ok {
|
|
155
|
+
issues = append(issues, "required command not run: "+command+" (run kkt validate --run)")
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if proof.Status != "passed" || proof.ExitCode != 0 {
|
|
159
|
+
issues = append(issues, "required command failed: "+command)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if proof.Fingerprint != fingerprint {
|
|
163
|
+
issues = append(issues, "required command proof is stale: "+command)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return issues
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func latestValidationCommandProofs(workspace string) (map[string]validationCommandProof, error) {
|
|
170
|
+
events, err := readEvents(workspace, 0)
|
|
171
|
+
if err != nil {
|
|
172
|
+
return nil, err
|
|
173
|
+
}
|
|
174
|
+
proofs := map[string]validationCommandProof{}
|
|
175
|
+
for _, event := range events {
|
|
176
|
+
if event.Type != "validation_command_passed" && event.Type != "validation_command_failed" {
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
proof := validationCommandProof{
|
|
180
|
+
Command: eventDataString(event.Data, "command"),
|
|
181
|
+
Status: eventDataString(event.Data, "status"),
|
|
182
|
+
ExitCode: eventDataInt(event.Data, "exit_code"),
|
|
183
|
+
DurationMS: int64(eventDataInt(event.Data, "duration_ms")),
|
|
184
|
+
Timestamp: eventDataString(event.Data, "timestamp"),
|
|
185
|
+
Log: eventDataString(event.Data, "log"),
|
|
186
|
+
Fingerprint: eventDataString(event.Data, "fingerprint"),
|
|
187
|
+
}
|
|
188
|
+
if proof.Command != "" {
|
|
189
|
+
proofs[proof.Command] = proof
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return proofs, nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func appendValidationCommandEvent(workspace string, proof validationCommandProof) error {
|
|
196
|
+
state, err := ReadState(workspace)
|
|
197
|
+
if err != nil {
|
|
198
|
+
return err
|
|
199
|
+
}
|
|
200
|
+
eventType := "validation_command_passed"
|
|
201
|
+
if proof.Status != "passed" || proof.ExitCode != 0 {
|
|
202
|
+
eventType = "validation_command_failed"
|
|
203
|
+
}
|
|
204
|
+
return appendWorkspaceEvent(workspace, state, eventType, map[string]any{
|
|
205
|
+
"command": proof.Command,
|
|
206
|
+
"status": proof.Status,
|
|
207
|
+
"exit_code": proof.ExitCode,
|
|
208
|
+
"duration_ms": proof.DurationMS,
|
|
209
|
+
"timestamp": proof.Timestamp,
|
|
210
|
+
"log": proof.Log,
|
|
211
|
+
"fingerprint": proof.Fingerprint,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func appendValidationCommandEvidence(workspace string, proof validationCommandProof) error {
|
|
216
|
+
file, err := os.OpenFile(filepath.Join(workspace, "evidence.md"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
|
217
|
+
if err != nil {
|
|
218
|
+
return err
|
|
219
|
+
}
|
|
220
|
+
defer file.Close()
|
|
221
|
+
_, err = fmt.Fprintf(file, "\n## Validation Command\n\n- command: `%s`\n- status: %s\n- exit_code: %d\n- duration_ms: %d\n- log: %s\n- timestamp: %s\n", proof.Command, proof.Status, proof.ExitCode, proof.DurationMS, proof.Log, proof.Timestamp)
|
|
222
|
+
return err
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func markEvidenceRecorded(workspace string) error {
|
|
226
|
+
path := filepath.Join(workspace, "evidence.md")
|
|
227
|
+
content, err := os.ReadFile(path)
|
|
228
|
+
if err != nil {
|
|
229
|
+
return err
|
|
230
|
+
}
|
|
231
|
+
next := strings.Replace(string(content), "Status: pending", "Status: recorded", 1)
|
|
232
|
+
if next == string(content) {
|
|
233
|
+
return nil
|
|
234
|
+
}
|
|
235
|
+
return os.WriteFile(path, []byte(next), 0o644)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func workingTreeFingerprint(projectRootDir, workspace string) (string, error) {
|
|
239
|
+
changed, err := changedGitPaths(projectRootDir)
|
|
240
|
+
if err != nil {
|
|
241
|
+
return "", err
|
|
242
|
+
}
|
|
243
|
+
contract, contractErr := readGuardrails(workspace)
|
|
244
|
+
allowed := []string{}
|
|
245
|
+
blocked := []string{}
|
|
246
|
+
if contractErr == nil {
|
|
247
|
+
allowed = contract.allowedPaths()
|
|
248
|
+
blocked = contract.blockedPaths()
|
|
249
|
+
}
|
|
250
|
+
var entries []string
|
|
251
|
+
for _, path := range changed {
|
|
252
|
+
if isKKTPath(path) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
if len(allowed) > 0 && !matchesAnyPathPattern(path, allowed) && !matchesAnyPathPattern(path, blocked) {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
fullPath := filepath.Join(projectRootDir, filepath.FromSlash(path))
|
|
259
|
+
payload, readErr := os.ReadFile(fullPath)
|
|
260
|
+
switch {
|
|
261
|
+
case readErr == nil:
|
|
262
|
+
sum := sha256.Sum256(payload)
|
|
263
|
+
entries = append(entries, path+"="+hex.EncodeToString(sum[:]))
|
|
264
|
+
case os.IsNotExist(readErr):
|
|
265
|
+
entries = append(entries, path+"=<deleted>")
|
|
266
|
+
default:
|
|
267
|
+
return "", readErr
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
sort.Strings(entries)
|
|
271
|
+
sum := sha256.Sum256([]byte(strings.Join(entries, "\n")))
|
|
272
|
+
return hex.EncodeToString(sum[:]), nil
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
func eventDataString(data map[string]any, key string) string {
|
|
276
|
+
value, _ := data[key].(string)
|
|
277
|
+
return value
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
func eventDataInt(data map[string]any, key string) int {
|
|
281
|
+
switch value := data[key].(type) {
|
|
282
|
+
case int:
|
|
283
|
+
return value
|
|
284
|
+
case int64:
|
|
285
|
+
return int(value)
|
|
286
|
+
case float64:
|
|
287
|
+
return int(value)
|
|
288
|
+
default:
|
|
289
|
+
return 0
|
|
290
|
+
}
|
|
291
|
+
}
|