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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ checks manifest and contract source legality before later workflow steps. `lint`
checks project conventions and risk rules, while `verify` executes the validation,
lint, build, and test evidence pipeline.

`gen` writes reproducible artifacts under generated targets such as
`contract/gen` and `internal/adapter/http/gen`. Use `nucleus gen --json` when
automation needs auditable evidence; the result includes
`result_kind: "nucleus.gen_result"`, `ok`, `source_hash`, generated `files`,
`summary`, and validation `diagnostics`.

For the bundled example, run `nucleus validate --dir example/hello-http`.
Successful human output includes a short validation summary; `--json` emits the
same result with stable `ok`, `summary`, and `diagnostics` fields.
Expand Down
51 changes: 51 additions & 0 deletions cmd/nucleus/internal/describe/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ func TestBuildOutputAddsDescribeMetadata(t *testing.T) {
if got := verification["result_kind"]; got != "nucleus.verify_result" {
t.Fatalf("verification.result_kind = %v, want nucleus.verify_result", got)
}
if got := verification["evidence_schema"]; got != verificationEvidenceSchema {
t.Fatalf("verification.evidence_schema = %v, want %s", got, verificationEvidenceSchema)
}
schemaPath := filepath.Join("..", "..", "..", "..", filepath.FromSlash(verificationEvidenceSchema))
if _, err := os.Stat(schemaPath); err != nil {
t.Fatalf("verification evidence schema %s is not readable: %v", schemaPath, err)
}
pipeline, ok := verification["pipeline"].([]map[string]any)
if !ok {
t.Fatalf("verification.pipeline has type %T, want []map[string]any", verification["pipeline"])
}
assertVerificationPipeline(t, pipeline)
}

func TestBuildOutputUsesDefaultSchemaVersion(t *testing.T) {
Expand Down Expand Up @@ -60,3 +72,42 @@ nucleus: {}
}
return dir
}

func assertVerificationPipeline(t *testing.T, pipeline []map[string]any) {
t.Helper()
want := []struct {
phase string
command string
}{
{phaseValidate, commandValidate},
{phaseLint, commandLintStrict},
{phaseGeneratedFreshness, commandDescribeJSON},
{phaseTidy, commandGoModTidy},
{phaseImport, commandGoListAll},
{phaseBuild, commandGoTestCompileOnly},
{phaseTest, commandGoTestAll},
}
if len(pipeline) != len(want) {
t.Fatalf("pipeline length = %d, want %d", len(pipeline), len(want))
}
for index, item := range pipeline {
if got := item["id"]; got != want[index].phase {
t.Fatalf("pipeline[%d].id = %v, want %s", index, got, want[index].phase)
}
if got := item["sequence"]; got != index+1 {
t.Fatalf("pipeline[%d].sequence = %v, want %d", index, got, index+1)
}
if got := item["phase"]; got != want[index].phase {
t.Fatalf("pipeline[%d].phase = %v, want %s", index, got, want[index].phase)
}
if got := item["command"]; got != want[index].command {
t.Fatalf("pipeline[%d].command = %v, want %s", index, got, want[index].command)
}
if got := item["schema_ref"]; got != verificationEvidenceSchema {
t.Fatalf("pipeline[%d].schema_ref = %v, want %s", index, got, verificationEvidenceSchema)
}
if got := item["produces"]; got != verificationResultKind {
t.Fatalf("pipeline[%d].produces = %v, want %s", index, got, verificationResultKind)
}
}
}
66 changes: 66 additions & 0 deletions cmd/nucleus/internal/gen/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package gen

import (
"errors"

"github.com/spf13/cobra"
)

// Config carries root-level flag values used by the gen command.
type Config struct {
Dir *string
}

type options struct {
json bool
pretty bool
http bool
grpc bool
errors bool
clients bool
clientLanguages []string
docs bool
typeScript bool
}

// ErrGenFailed is returned when generation cannot produce valid artifacts.
var ErrGenFailed = errors.New("generation failed")

// NewCommand creates the gen subcommand.
func NewCommand(config Config) *cobra.Command {
opts := &options{}
cmd := &cobra.Command{
Use: commandUseGen,
Short: commandShortGen,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
result, err := run(config, opts)
if opts.json {
if renderErr := renderJSON(cmd.OutOrStdout(), result, opts.pretty); renderErr != nil {
return renderErr
}
} else {
renderHuman(cmd.OutOrStdout(), cmd.ErrOrStderr(), result)
}
return err
},
}
cmd.Flags().BoolVar(&opts.json, flagJSON, false, flagHelpJSON)
cmd.Flags().BoolVar(&opts.pretty, flagPretty, false, flagHelpPretty)
cmd.Flags().BoolVar(&opts.http, flagHTTP, false, flagHelpHTTP)
cmd.Flags().BoolVar(&opts.grpc, flagGRPC, false, flagHelpGRPC)
cmd.Flags().BoolVar(&opts.errors, flagErrors, false, flagHelpErrors)
cmd.Flags().BoolVar(&opts.clients, flagClients, false, flagHelpClients)
cmd.Flags().StringSliceVar(&opts.clientLanguages, flagClientLanguage, nil, flagHelpClientLanguage)
cmd.Flags().BoolVar(&opts.docs, flagDocs, false, flagHelpDocs)
cmd.Flags().BoolVar(&opts.typeScript, flagTypeScript, false, flagHelpTypeScript)
return cmd
}

func stringValue(value *string, fallback string) string {
if value == nil {
return fallback
}
return *value
}
251 changes: 251 additions & 0 deletions cmd/nucleus/internal/gen/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package gen

import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
)

func TestCommandJSONSuccess(t *testing.T) {
dir := t.TempDir()
writeService(t, dir)

cmd := NewCommand(Config{Dir: &dir})
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs([]string{"--json", "--http", "--errors", "--docs", "--typescript", "--clients", "--client-language", "typescript"})

if err := cmd.Execute(); err != nil {
t.Fatalf("execute gen: %v", err)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}

var output struct {
ResultKind string `json:"result_kind"`
OK bool `json:"ok"`
SourceHash string `json:"source_hash"`
Summary genSummary `json:"summary"`
Files []string `json:"files"`
}
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
t.Fatalf("decode JSON: %v\n%s", err, stdout.String())
}
if output.ResultKind != resultKindGen {
t.Fatalf("result_kind = %q, want %q", output.ResultKind, resultKindGen)
}
if !output.OK {
t.Fatal("ok = false, want true")
}
if output.SourceHash == "" {
t.Fatal("source_hash is empty")
}
if output.Summary.Files != len(output.Files) {
t.Fatalf("summary.files = %d, want %d", output.Summary.Files, len(output.Files))
}
for _, want := range []string{
"contract/gen/endpoints.go",
"contract/gen/errors.go",
"contract/gen/contract_source.go",
"contract/gen/contract.md",
"contract/gen/types.ts",
"internal/adapter/http/gen/routes.gen.go",
"sdk/typescript/client.ts",
"sdk/typescript/.nucleus-source.sha256",
} {
if !containsString(output.Files, want) {
t.Fatalf("files = %#v, want %s", output.Files, want)
}
}
}

func TestCommandJSONValidationFailure(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "nucleus.yaml", `schema_version: "1.0"
service:
version: "0.1.0"
ai:
intent: test
`)

cmd := NewCommand(Config{Dir: &dir})
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs([]string{"--json"})

err := cmd.Execute()
if !errors.Is(err, ErrGenFailed) {
t.Fatalf("execute gen error = %v, want ErrGenFailed", err)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty for JSON output", stderr.String())
}

var output struct {
OK bool `json:"ok"`
Diagnostics []struct {
Code string `json:"code"`
} `json:"diagnostics"`
}
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
t.Fatalf("decode JSON: %v\n%s", err, stdout.String())
}
if output.OK {
t.Fatal("ok = true, want false")
}
found := false
for _, item := range output.Diagnostics {
if item.Code == "manifest.service_name_required" {
found = true
break
}
}
if !found {
t.Fatalf("diagnostics = %#v, want manifest.service_name_required", output.Diagnostics)
}
}

func TestCommandHumanSuccessOutput(t *testing.T) {
dir := t.TempDir()
writeService(t, dir)

cmd := NewCommand(Config{Dir: &dir})
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs([]string{"--http", "--errors"})

if err := cmd.Execute(); err != nil {
t.Fatalf("execute gen: %v", err)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
output := stdout.String()
for _, want := range []string{
"OK",
"generated:",
"source_hash:",
"contract/gen/endpoints.go",
"internal/adapter/http/gen/routes.gen.go",
} {
if !strings.Contains(output, want) {
t.Fatalf("stdout = %q, want %q", output, want)
}
}
}

func TestCommandJSONUnsupportedClientLanguage(t *testing.T) {
dir := t.TempDir()
writeService(t, dir)

cmd := NewCommand(Config{Dir: &dir})
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs([]string{"--json", "--clients", "--client-language", "ruby"})

err := cmd.Execute()
if !errors.Is(err, ErrGenFailed) {
t.Fatalf("execute gen error = %v, want ErrGenFailed", err)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty for JSON output", stderr.String())
}

var output struct {
OK bool `json:"ok"`
Diagnostics []struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"diagnostics"`
}
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
t.Fatalf("decode JSON: %v\n%s", err, stdout.String())
}
if output.OK {
t.Fatal("ok = true, want false")
}
if len(output.Diagnostics) == 0 || output.Diagnostics[0].Code != "gen.failed" {
t.Fatalf("diagnostics = %#v, want gen.failed", output.Diagnostics)
}
if !strings.Contains(output.Diagnostics[0].Message, "unsupported client language") {
t.Fatalf("message = %q, want unsupported language", output.Diagnostics[0].Message)
}
}

func writeService(t *testing.T, dir string) {
t.Helper()
writeFile(t, dir, "nucleus.yaml", `schema_version: "1.0"
service:
name: demo
version: "0.1.0"
ai:
intent: test
generated:
- contract/gen
- internal/adapter/http/gen
- sdk/typescript
capabilities:
- http
`)
writeFile(t, dir, "api/openapi.yaml", `openapi: 3.0.3
paths:
/hello/{name}:
get:
operationId: getHello
parameters:
- name: name
in: path
required: true
schema:
type: string
responses:
"200":
description: ok
components:
schemas:
Greeting:
type: object
required: [message]
properties:
message:
type: string
`)
writeFile(t, dir, "api/errors.yaml", `errors:
- code: 1001
message: greeting failed
http_status: 500
`)
}

func writeFile(t *testing.T, dir string, name string, data string) {
t.Helper()
path := filepath.Join(dir, name)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(data), 0o600); err != nil {
t.Fatal(err)
}
}

func containsString(values []string, want string) bool {
for _, value := range values {
if value == want {
return true
}
}
return false
}
Loading
Loading