Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions STRUKTLY_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Struktly

**Struktly** is a governed, fail-first, deterministic code generation system.

It operates on the principle that **no code is better than bad code**.

## Core Principles

1. **Fail-First**: If the generated code does not compile, pass tests, or satisfy policy, it is **discarded**. There are no silent retries.
2. **Determinism**: The same inputs (Spec + Policy) always produce the same scaffolding. AI is only used for bounded logic synthesis within strict templates.
3. **Auditability**: Every run produces an audit trail (`audit.json`, `policy.json`, `verification.json`). If verification fails, you get an audit log explaining why, but no code.
4. **No Hallucinated Intent**: If the system is unsure (simulated by "ambiguous" input), it returns `NEEDS_HUMAN_REVIEW` rather than guessing.

## Architecture

- **CLI**: The entry point.
- **Orchestrator**: Manages the pipeline (Input -> Policy -> Generate -> Synthesize -> Verify -> Output).
- **Generator**: Uses `text/template` to create deterministic `main.go` and handler skeletons.
- **Synthesizer**: Fills in handler logic and tests. In a real system, this calls an LLM. Here, it is mocked to demonstrate success/failure modes.
- **Gates**: Runs `go fmt`, `go vet`, and `go test` inside a sandbox (temp directory).
- **Policy Engine**: Enforces constraints (e.g., max handlers).

## Usage

Build the tool:
```bash
go build -o struktly ./cmd/struktly
```

Run a generation:
```bash
./struktly --spec examples/struktly/petstore.yaml --policy examples/struktly/policy.yaml --out output_dir
```

## Failure Scenarios (Demo)

The included `demo_struktly.sh` script demonstrates three scenarios:

1. **Success**: Valid inputs produce a working Go service with passing tests.
2. **Failure**: Inputs that trigger "bad code" generation (simulated) result in **no output code**, only audit logs.
- *Why?* Because `go vet` or `go test` failed.
3. **Ambiguity**: Inputs that trigger uncertainty result in `NEEDS_HUMAN_REVIEW`.
- *Why?* The system refuses to guess business logic.

## What is NOT Supported

- **Retries**: If it fails, fix the input or the prompt. Do not loop until it "looks right".
- **AI Architecture**: The AI cannot add new files, change the router, or pick dependencies. It only fills the function bodies it is assigned.
- **Framework Magic**: The generated code is standard library Go.
43 changes: 43 additions & 0 deletions cmd/struktly/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"fmt"
"os"

"github.com/froppa/stackkit/internal/struktly/orchestrator"
"github.com/spf13/cobra"
)

func main() {
var specPath string
var policyPath string
var outDir string

var rootCmd = &cobra.Command{
Use: "struktly",
Short: "Struktly Code Generation",
Run: func(cmd *cobra.Command, args []string) {
if err := orchestrator.Run(specPath, policyPath, outDir); err != nil {
// We don't necessarily exit 1 if it's a "controlled failure" (e.g. policy violation),
// but generally if the tool fails to produce the expected output, exit !0 is good for scripts.
// However, the prompt says "OR a failure report". If we produce a failure report, maybe we exit 0?
// "FAIL-FIRST IS A FEATURE... DISCARD IT".
// "If verification fails, the run fails regardless of AI confidence"
// I will print the error and exit 1 so CI pipelines know it failed.
fmt.Fprintf(os.Stderr, "Struktly Run Failed: %v\n", err)
os.Exit(1)
}
fmt.Println("Struktly Run Successful.")
},
}

rootCmd.Flags().StringVar(&specPath, "spec", "", "Path to OpenAPI spec")
rootCmd.Flags().StringVar(&policyPath, "policy", "", "Path to Policy file")
rootCmd.Flags().StringVar(&outDir, "out", "output", "Output directory")
_ = rootCmd.MarkFlagRequired("spec")
_ = rootCmd.MarkFlagRequired("policy")

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
40 changes: 40 additions & 0 deletions demo_struktly.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
set -e

# Build
echo "Building struktly..."
go build -o struktly ./cmd/struktly

echo ">>> RUN 1: SUCCESS CASE"
rm -rf out_success
./struktly --spec examples/struktly/petstore.yaml --policy examples/struktly/policy.yaml --out out_success
ls -l out_success
# Check for code
if [ ! -f out_success/main.go ]; then echo "FAIL: main.go missing"; exit 1; fi
if [ ! -f out_success/audit.json ]; then echo "FAIL: audit.json missing"; exit 1; fi

echo ">>> RUN 2: FAILURE CASE (Bad Code)"
rm -rf out_fail
set +e
./struktly --spec examples/struktly/petstore_fail.yaml --policy examples/struktly/policy.yaml --out out_fail
EXIT_CODE=$?
set -e
if [ $EXIT_CODE -eq 0 ]; then echo "FAIL: Should have failed"; exit 1; fi
# Check NO code
if [ -f out_fail/main.go ]; then echo "FAIL: main.go should NOT exist"; exit 1; fi
# Check Audit exists
if [ ! -f out_fail/audit.json ]; then echo "FAIL: audit.json missing"; exit 1; fi
grep "FAIL" out_fail/audit.json > /dev/null
echo "Run 2 Verified Correctly"

echo ">>> RUN 3: AMBIGUOUS CASE"
rm -rf out_ambiguous
set +e
./struktly --spec examples/struktly/petstore_ambiguous.yaml --policy examples/struktly/policy.yaml --out out_ambiguous
EXIT_CODE=$?
set -e
if [ $EXIT_CODE -eq 0 ]; then echo "FAIL: Should have failed"; exit 1; fi
grep "NEEDS_HUMAN_REVIEW" out_ambiguous/audit.json > /dev/null
echo "Run 3 Verified Correctly"

echo "ALL DEMOS PASSED"
9 changes: 9 additions & 0 deletions examples/struktly/petstore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
openapi: 3.0.0
info:
title: Petstore Service
version: 1.0.0
paths:
/pets:
get:
operationId: ListPets
summary: List all pets
9 changes: 9 additions & 0 deletions examples/struktly/petstore_ambiguous.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
openapi: 3.0.0
info:
title: Petstore ambiguous Service
version: 1.0.0
paths:
/pets:
get:
operationId: ListPets
summary: List all pets
9 changes: 9 additions & 0 deletions examples/struktly/petstore_fail.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
openapi: 3.0.0
info:
title: Petstore fail Service
version: 1.0.0
paths:
/pets:
get:
operationId: ListPets
summary: List all pets
3 changes: 3 additions & 0 deletions examples/struktly/policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
max_handlers: 5
required:
- tests
34 changes: 34 additions & 0 deletions internal/struktly/audit/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package audit

import (
"time"
)

type AuditLog struct {
Timestamp time.Time `json:"timestamp"`
Inputs InputHashes `json:"inputs"`
Policy PolicyResult `json:"policy"`
Generation GenerationResult `json:"generation"`
Verification VerificationResult `json:"verification"`
Outcome string `json:"outcome"` // SUCCESS, FAIL, REVIEW
}

type InputHashes struct {
SpecHash string `json:"spec_hash"`
PolicyHash string `json:"policy_hash"`
}

type PolicyResult struct {
Passed bool `json:"passed"`
Reason string `json:"reason,omitempty"`
}

type GenerationResult struct {
Prompts []string `json:"prompts"`
Model string `json:"model"`
}

type VerificationResult struct {
Passed bool `json:"passed"`
Output string `json:"output,omitempty"`
}
63 changes: 63 additions & 0 deletions internal/struktly/gates/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package gates

import (
"bytes"
"fmt"
"os/exec"
)

type Runner struct{}

func New() *Runner {
return &Runner{}
}

// Verify runs go mod init, go fmt, go vet, and go test.
// Returns the combined output and an error if any step fails.
func (r *Runner) Verify(dir string) (string, error) {
var combinedOutput bytes.Buffer

// Helper to run and log
run := func(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Dir = dir
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr

combinedOutput.WriteString(fmt.Sprintf("Running: %s %v\n", name, args))
err := cmd.Run()

combinedOutput.Write(out.Bytes())
combinedOutput.Write(stderr.Bytes())
combinedOutput.WriteString("\n")

if err != nil {
return fmt.Errorf("%s failed: %w", name, err)
}
return nil
}

// 1. Init Module
if err := run("go", "mod", "init", "generated"); err != nil {
return combinedOutput.String(), err
}

// 2. Fmt
if err := run("go", "fmt", "./..."); err != nil {
return combinedOutput.String(), err
}

// 3. Vet
if err := run("go", "vet", "./..."); err != nil {
return combinedOutput.String(), err
}

// 4. Test
if err := run("go", "test", "./..."); err != nil {
return combinedOutput.String(), err
}

return combinedOutput.String(), nil
}
48 changes: 48 additions & 0 deletions internal/struktly/generator/renderer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package generator

import (
"bytes"
"fmt"
"text/template"

"github.com/froppa/stackkit/internal/struktly/ir"
)

func Render(svc *ir.Service) (map[string]string, error) {
files := make(map[string]string)

// Render Main
mainContent, err := renderTemplate("main", MainTemplate, svc)
if err != nil {
return nil, err
}
files["main.go"] = mainContent

// Render Handlers
handlersContent, err := renderTemplate("handlers", HandlersTemplate, svc)
if err != nil {
return nil, err
}
files["handlers.go"] = handlersContent

// Render Tests
testContent, err := renderTemplate("main_test", TestTemplate, svc)
if err != nil {
return nil, err
}
files["main_test.go"] = testContent

return files, nil
}

func renderTemplate(name, tmplStr string, data interface{}) (string, error) {
tmpl, err := template.New(name).Parse(tmplStr)
if err != nil {
return "", fmt.Errorf("failed to parse template %s: %w", name, err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("failed to execute template %s: %w", name, err)
}
return buf.String(), nil
}
51 changes: 51 additions & 0 deletions internal/struktly/generator/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package generator

const MainTemplate = `package main

import (
"log"
"net/http"
)

func main() {
mux := http.NewServeMux()
{{- range .Handlers }}
mux.HandleFunc("{{ .Path }}", {{ .Name }})
{{- end }}

log.Println("Starting {{ .Name }} on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
`

const HandlersTemplate = `package main

import (
"net/http"
)
{{ range .Handlers }}
// {{ .Name }} handles {{ .Method }} {{ .Path }}
// {{ .Description }}
func {{ .Name }}(w http.ResponseWriter, r *http.Request) {
// AI_LOGIC_START: {{ .Name }}
// AI_LOGIC_END: {{ .Name }}
}
{{ end }}
`

const TestTemplate = `package main

import (
"net/http"
"net/http/httptest"
"testing"
)
{{ range .Handlers }}
func Test{{ .Name }}(t *testing.T) {
// AI_TEST_START: {{ .Name }}
// AI_TEST_END: {{ .Name }}
}
{{ end }}
`
Loading