@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.
Files changed (50) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +275 -0
  3. package/assets/kkt-readme-modern.png +0 -0
  4. package/bin/kkt-install.mjs +25 -0
  5. package/cmd/kkt/main.go +15 -0
  6. package/go.mod +3 -0
  7. package/internal/workflow/cli.go +349 -0
  8. package/internal/workflow/cli_test.go +1266 -0
  9. package/internal/workflow/guardrails.go +900 -0
  10. package/internal/workflow/init.go +164 -0
  11. package/internal/workflow/init_test.go +113 -0
  12. package/internal/workflow/operations.go +1855 -0
  13. package/internal/workflow/validation_commands.go +291 -0
  14. package/internal/workflow/workspace.go +802 -0
  15. package/internal/workflow/workspace_test.go +285 -0
  16. package/package.json +45 -0
  17. package/scripts/install-cli.sh +210 -0
  18. package/scripts/install.sh +644 -0
  19. package/skills/kkt/SKILL.md +74 -0
  20. package/skills/kkt/references/discovery-tooling.md +53 -0
  21. package/skills/kkt/references/feature-optimization-model.md +120 -0
  22. package/skills/kkt/references/kkt-kernel.md +58 -0
  23. package/skills/kkt/references/layered-modeling-methods.md +101 -0
  24. package/skills/kkt/references/plan-assimilation.md +46 -0
  25. package/skills/kkt/references/schemas.md +231 -0
  26. package/skills/kkt/references/state-contract.md +76 -0
  27. package/skills/kkt-loop/SKILL.md +99 -0
  28. package/skills/kkt-loop/references/discovery-tooling.md +53 -0
  29. package/skills/kkt-loop/references/feature-optimization-model.md +120 -0
  30. package/skills/kkt-loop/references/kkt-kernel.md +58 -0
  31. package/skills/kkt-loop/references/layered-modeling-methods.md +101 -0
  32. package/skills/kkt-loop/references/plan-assimilation.md +46 -0
  33. package/skills/kkt-loop/references/schemas.md +231 -0
  34. package/skills/kkt-loop/references/state-contract.md +76 -0
  35. package/skills/kkt-model/SKILL.md +76 -0
  36. package/skills/kkt-model/references/discovery-tooling.md +53 -0
  37. package/skills/kkt-model/references/feature-optimization-model.md +120 -0
  38. package/skills/kkt-model/references/kkt-kernel.md +58 -0
  39. package/skills/kkt-model/references/layered-modeling-methods.md +101 -0
  40. package/skills/kkt-model/references/plan-assimilation.md +46 -0
  41. package/skills/kkt-model/references/schemas.md +231 -0
  42. package/skills/kkt-model/references/state-contract.md +76 -0
  43. package/skills/kkt-run/SKILL.md +62 -0
  44. package/skills/kkt-run/references/discovery-tooling.md +53 -0
  45. package/skills/kkt-run/references/feature-optimization-model.md +120 -0
  46. package/skills/kkt-run/references/kkt-kernel.md +58 -0
  47. package/skills/kkt-run/references/layered-modeling-methods.md +101 -0
  48. package/skills/kkt-run/references/plan-assimilation.md +46 -0
  49. package/skills/kkt-run/references/schemas.md +231 -0
  50. 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
+ }