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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ new runbook (and its skill) whenever you do a multi-step operational task a seco
### Extending the CLI
- **Add a new command** (Cobra wiring, help, MCP exposure) → [docs/runbooks/add-a-command.md](docs/runbooks/add-a-command.md)
- **Scaffold a solution** (`codefly add solution`: the one shape, its two services, and what the composition still owes it) → [docs/commands.md#add-solution](docs/commands.md)
- **Check a solution conforms** (`codefly doctor solution`: stable diagnostic codes, `--json`; the shape is checkable, not a survey) → [docs/commands.md](docs/commands.md)
- **Rebuild the CLI and agents from local source** → [docs/runbooks/update-agents.md](docs/runbooks/update-agents.md)
- **Point a workspace at a coordinate** (import explicit `codefly/coordinate/v1` environment declarations) → [docs/commands.md#codefly-environment](docs/commands.md)
- **Export a module's API contracts** → [docs/commands.md#generate-contracts](docs/commands.md#generate-contracts)
Expand Down
237 changes: 237 additions & 0 deletions cmd/doctor_solution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"

"github.com/codefly-dev/cli/cmd/common"
"github.com/codefly-dev/cli/pkg/solutionshape"
"github.com/codefly-dev/core/resources"
"github.com/spf13/cobra"
)

var (
doctorSolutionJSON bool
doctorSolutionDir string
doctorSolutionRequire bool
)

// solutionManifestName is the file whose presence makes a module a solution.
const solutionManifestName = "solution.codefly.yaml"

// DoctorSolutionCmd judges a solution against the one shape a solution has.
//
// It exists because the shape was, until now, checkable only against
// somebody's survey of the solutions that happened to exist — which is how one
// fleet reached nine solutions in four different states, two of them unable to
// receive a person capability at all with nothing failing to say so. A shape
// nothing can check is a convention, and a convention drifts.
var DoctorSolutionCmd = &cobra.Command{
Use: "solution [module]",
// A non-conforming solution is an expected outcome, not a usage mistake.
SilenceUsage: true,
Short: "Check a solution against the one shape a solution has, without changing anything",
Long: `Judge a solution module against the shape ` + "`codefly add solution`" + ` generates.

With no argument, every module in the workspace is judged and the ones that
ship no solution manifest are reported as "not a solution" rather than as
broken. Name a module to judge only that one.

It is strictly read-only: it opens files and writes none.

Every rule is one that silently broke something. The two worth knowing:

owner_aware The authority issues a person a capability at a solution ONLY
when its module contract declares owner_aware: true, and
refuses by name otherwise. Without it the solution receives no
owner-selected scope and can act for nobody — and nothing
fails at build time to tell you.

module edges A solution reaches every module through the host's door,
which checks the call. A service-dependency naming a module
renders into a mesh allow rule straight to that module's
service, past the check.

Exit codes:
0 every judged solution conforms (warnings allowed)
1 at least one check failed, or the command itself failed

With --json, a versioned report per module is printed to stdout:
{schema_version, module, dir, is_solution, status: ok|fail,
checks: [{code, status, message, remediation?}]}

Stable diagnostic codes: not_a_solution, solution_manifest_unreadable,
consumed_api_unnamed, service_entry_missing, service_entry_not_the_entry,
entry_service_missing, entry_service_unreadable, module_identity_missing,
module_edge_declared, entry_endpoint_missing, module_contract_missing,
module_contract_unreadable, owner_aware_missing, module_package_missing,
module_package_unreadable, module_package_publishes_shell,
frontend_declaration_missing, frontend_declaration_unreadable,
page_shell_missing, module_interface_missing, entry_endpoint_not_exported,
page_shell_exported_too_widely.`,
Example: ` # Judge every solution in the workspace.
codefly doctor solution

# Judge one, and script against the result.
codefly doctor solution wiki --json

# Gate a consumer repository's tree, where a solution need not be a workspace.
for s in solutions/*/; do codefly doctor solution --dir "$s" || exit 1; done

# The same gate, where every directory walked must BE a solution: this
# catches a solution whose manifest is missing, which otherwise reads as
# "not a solution" and passes.
codefly doctor solution --dir solutions/robin-builder --require-solution`,
Args: cobra.MaximumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
ctx, done := common.NewContext()
defer done()

// --dir judges a module directory on its own, with no workspace
// resolution. A consumer repository gating `solutions/*/` has a tree
// of module directories and not necessarily a workspace at each one:
// obin-ai/core-solutions' excel-extract is a real solution with no
// workspace.codefly.yaml, and requiring one made it the single
// solution that could not be judged at all.
if doctorSolutionDir != "" {
if len(args) > 0 {
return fmt.Errorf("--dir judges the directory itself, so it takes no module name")
}
absolute, err := filepath.Abs(doctorSolutionDir)
if err != nil {
return fmt.Errorf("cannot resolve %s: %w", doctorSolutionDir, err)
}
name := filepath.Base(absolute)
if module, loadErr := resources.LoadModuleFromDir(ctx, absolute); loadErr == nil && module.Name != "" {
name = module.Name
}
report := solutionshape.Inspect(name, absolute)
reports := []*solutionshape.Report{report}
if doctorSolutionJSON {
return printSolutionReportsJSON(reports)
}
// The caller named this directory, so "it is not a solution" is an
// answer and not a vacuous pass. A tree gate looping solutions/*/
// meets directories that are not solutions — core-solutions'
// client-engagement is one — and must not fail on them.
if !report.IsSolution {
if doctorSolutionRequire {
return fmt.Errorf("%s ships no %s, so it is not a solution — and --require-solution says it must be", report.Module, solutionManifestName)
}
fmt.Printf("OK %s: not a solution (no %s)\n", report.Module, solutionManifestName)
return nil
}
return printSolutionReports(reports)
}

workspace, err := common.LoadWorkspace(ctx)
if err != nil {
return fmt.Errorf("cannot load workspace: %w", err)
}

var reports []*solutionshape.Report
if len(args) == 1 {
module, loadErr := workspace.LoadModuleFromName(ctx, args[0])
if loadErr != nil {
return fmt.Errorf("cannot load module %s: %w", args[0], loadErr)
}
reports = append(reports, solutionshape.Inspect(module.Name, module.Dir()))
} else {
names := make([]string, 0, len(workspace.Modules))
for _, reference := range workspace.Modules {
if reference != nil {
names = append(names, reference.Name)
}
}
sort.Strings(names)
for _, name := range names {
module, loadErr := workspace.LoadModuleFromName(ctx, name)
if loadErr != nil {
// A module that does not resolve is the workspace's
// problem, not the shape's; `doctor workspace` reports it.
continue
}
reports = append(reports, solutionshape.Inspect(module.Name, module.Dir()))
}
}

if doctorSolutionJSON {
return printSolutionReportsJSON(reports)
}
return printSolutionReports(reports)
},
}

func printSolutionReportsJSON(reports []*solutionshape.Report) error {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
for _, report := range reports {
if err := encoder.Encode(report); err != nil {
return err
}
}
return solutionReportsOutcome(reports)
}

func printSolutionReports(reports []*solutionshape.Report) error {
solutions := 0
for _, report := range reports {
if !report.IsSolution {
continue
}
solutions++
fmt.Printf("\n%s %s\n", statusGlyph(report.Status), report.Module)
for _, check := range report.Checks {
if check.Status == solutionshape.StatusOK {
continue
}
fmt.Printf(" %s %s: %s\n", statusGlyph(check.Status), check.Code, check.Message)
if check.Remediation != "" {
fmt.Printf(" %s\n", check.Remediation)
}
}
if report.Conforms() {
fmt.Printf(" conforms to the solution shape\n")
}
}
if solutions == 0 {
// Never a vacuous pass: a run that judged nothing must not read as a
// run that found nothing wrong.
return fmt.Errorf("no solution was found to judge; a module is a solution when it ships a solution manifest")
}
return solutionReportsOutcome(reports)
}

func solutionReportsOutcome(reports []*solutionshape.Report) error {
var broken []string
for _, report := range reports {
if report.IsSolution && !report.Conforms() {
broken = append(broken, report.Module)
}
}
if len(broken) > 0 {
return fmt.Errorf("%d solution(s) do not conform to the shape: %v", len(broken), broken)
}
return nil
}

func statusGlyph(status solutionshape.Status) string {
switch status {
case solutionshape.StatusFail:
return "FAIL"
case solutionshape.StatusWarn:
return "WARN"
default:
return "OK "
}
}

func init() {
DoctorSolutionCmd.Flags().BoolVar(&doctorSolutionJSON, "json", false, "Print a versioned report per module to stdout")
DoctorSolutionCmd.Flags().BoolVar(&doctorSolutionRequire, "require-solution", false, "With --dir, fail when the directory ships no solution manifest instead of reporting it as not a solution. A tree gate uses it to say every directory it walks must BE a solution")
DoctorSolutionCmd.Flags().StringVar(&doctorSolutionDir, "dir", "", "Judge this module directory on its own, without resolving a workspace. For a consumer repository gating a tree of solutions, where a directory need not be a workspace")
DoctorCmd.AddCommand(DoctorSolutionCmd)
}
60 changes: 60 additions & 0 deletions cmd/doctor_solution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cmd

import (
"strings"
"testing"

"github.com/codefly-dev/cli/pkg/solutionshape"
)

func TestDoctorSolutionIsRegisteredAndHelpStandsAlone(t *testing.T) {
var found bool
for _, child := range DoctorCmd.Commands() {
if child.Name() == "solution" {
found = true
}
}
if !found {
t.Fatal("doctor solution is not registered under doctor")
}
if DoctorSolutionCmd.RunE == nil || DoctorSolutionCmd.Run != nil {
t.Error("doctor solution must use RunE so the root owns exit codes")
}
for name, text := range map[string]string{
"Short": DoctorSolutionCmd.Short, "Long": DoctorSolutionCmd.Long, "Example": DoctorSolutionCmd.Example,
} {
if strings.TrimSpace(text) == "" {
t.Errorf("%s is empty; help must stand alone without network access", name)
}
}
if !DoctorSolutionCmd.SilenceUsage {
t.Error("a non-conforming solution is an outcome, not a usage mistake")
}
for _, flag := range []string{"json", "dir", "require-solution"} {
if DoctorSolutionCmd.Flags().Lookup(flag) == nil {
t.Errorf("--%s is not registered", flag)
}
}
}

// The help promises stable diagnostic codes, so a caller scripts against them.
// A code that exists in the package and is not documented cannot be relied on.
func TestEveryDiagnosticCodeIsDocumented(t *testing.T) {
for _, code := range []string{
solutionshape.CodeNotASolution, solutionshape.CodeManifestUnreadable,
solutionshape.CodeConsumedAPIUnnamed, solutionshape.CodeServiceEntryMissing,
solutionshape.CodeServiceEntryNotTheEntry, solutionshape.CodeEntryServiceMissing,
solutionshape.CodeEntryServiceUnreadable, solutionshape.CodeModuleIdentityMissing,
solutionshape.CodeModuleEdgeDeclared, solutionshape.CodeEntryEndpointMissing,
solutionshape.CodeContractMissing, solutionshape.CodeContractUnreadable,
solutionshape.CodeOwnerAwareMissing, solutionshape.CodePackageMissing,
solutionshape.CodePackageUnreadable, solutionshape.CodePackagePublishesShell,
solutionshape.CodeFrontendMissing, solutionshape.CodeFrontendUnreadable,
solutionshape.CodePageServiceMissing, solutionshape.CodeInterfaceMissing,
solutionshape.CodeEntryNotExported, solutionshape.CodePageExportedTooWidely,
} {
if !strings.Contains(DoctorSolutionCmd.Long, code) {
t.Errorf("diagnostic code %q is not documented in the command's help", code)
}
}
}
58 changes: 58 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2476,8 +2476,66 @@ codefly doctor [flags]

Subcommands:

- [`codefly doctor solution`](#codefly-doctor-solution)
- [`codefly doctor workspace`](#codefly-doctor-workspace)

## `codefly doctor solution`

Check a solution against the one shape a solution has, without changing anything

```
Judge a solution module against the shape `codefly add solution` generates.

With no argument, every module in the workspace is judged and the ones that
ship no solution manifest are reported as "not a solution" rather than as
broken. Name a module to judge only that one.

It is strictly read-only: it opens files and writes none.

Every rule is one that silently broke something. The two worth knowing:

owner_aware The authority issues a person a capability at a solution ONLY
when its module contract declares owner_aware: true, and
refuses by name otherwise. Without it the solution receives no
owner-selected scope and can act for nobody — and nothing
fails at build time to tell you.

module edges A solution reaches every module through the host's door,
which checks the call. A service-dependency naming a module
renders into a mesh allow rule straight to that module's
service, past the check.

Exit codes:
0 every judged solution conforms (warnings allowed)
1 at least one check failed, or the command itself failed

With --json, a versioned report per module is printed to stdout:
{schema_version, module, dir, is_solution, status: ok|fail,
checks: [{code, status, message, remediation?}]}

Stable diagnostic codes: not_a_solution, solution_manifest_unreadable,
consumed_api_unnamed, service_entry_missing, service_entry_not_the_entry,
entry_service_missing, entry_service_unreadable, module_identity_missing,
module_edge_declared, entry_endpoint_missing, module_contract_missing,
module_contract_unreadable, owner_aware_missing, module_package_missing,
module_package_unreadable, module_package_publishes_shell,
frontend_declaration_missing, frontend_declaration_unreadable,
page_shell_missing, module_interface_missing, entry_endpoint_not_exported,
page_shell_exported_too_widely.
```

```
codefly doctor solution [module]
```

Flags:

```
--dir string Judge this module directory on its own, without resolving a workspace. For a consumer repository gating a tree of solutions, where a directory need not be a workspace
--json Print a versioned report per module to stdout
--require-solution With --dir, fail when the directory ships no solution manifest instead of reporting it as not a solution. A tree gate uses it to say every directory it walks must BE a solution
```

## `codefly doctor workspace`

Validate workspace paths, configuration, and agent readiness without changes
Expand Down
Loading
Loading