From f34f726ebfbfb711ecb0a3fa628b390e1138ad22 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Mon, 10 Aug 2026 11:50:19 +0000 Subject: [PATCH] Merge lane/readability-shared-vocabulary: centralize shared application vocabularies --- internal/diff/safe.go | 28 ++++++++++++++++++++++++++++ internal/failure/error.go | 11 +++++++++++ internal/quality/architecture_test.go | 5 +++-- internal/state/repositories.go | 10 +++++----- internal/application/add/types.go | 18 ++++++------------ internal/application/apply/types.go | 18 ++++++------------ internal/application/apply/types_test.go | 2 +- internal/application/inspect/diff.go | 23 ++++------------------- internal/application/outcome/types.go | 22 ++++++++++++++++++++++ internal/application/validate/service.go | 2 +- internal/application/validate/types.go | 12 ------------ 11 file(s) changed, 87 insertion(s)(+), 64 deletion(s)(-) diff --git a/internal/diff/safe.go b/internal/diff/safe.go --- a/internal/diff/safe.go +++ b/internal/diff/safe.go @@ -34,6 +34,34 @@ TagSecret ) +// String returns the stable lowercase name of one safe difference category. +func (tag Tag) String() string { + switch tag { + case TagText: + return "text" + case TagBinary: + return "binary" + case TagSecret: + return "secret" + default: + return "none" + } +} + +// ParseTag maps a stable lowercase name to its safe difference category. +func ParseTag(name string) Tag { + switch name { + case "text": + return TagText + case "binary": + return TagBinary + case "secret": + return TagSecret + default: + return TagNone + } +} + // Valid reports whether tag is one of the supported constants. func (t Tag) Valid() bool { return t >= TagNone && t <= TagSecret } diff --git a/internal/failure/error.go b/internal/failure/error.go --- a/internal/failure/error.go +++ b/internal/failure/error.go @@ -3,6 +3,7 @@ import ( "errors" "fmt" + "os" ) // Kind categorizes a failure for the CLI exit mapper. It carries no numeric @@ -50,6 +51,16 @@ // is a leaf; pass a lower-level error to preserve its chain. func New(kind Kind, message string, cause error) *Error { return &Error{Kind: kind, Message: message, Cause: cause} +} + +// FromPathError categorizes a compile failure: filesystem path errors are +// operational, while repository validation errors are invalid input. +func FromPathError(message string, cause error) *Error { + var pathError *os.PathError + if errors.As(cause, &pathError) { + return New(Operational, message, cause) + } + return New(InvalidInput, message, cause) } // HasKind walks an error chain and any errors.Join group, returning the first diff --git a/internal/quality/architecture_test.go b/internal/quality/architecture_test.go --- a/internal/quality/architecture_test.go +++ b/internal/quality/architecture_test.go @@ -39,8 +39,9 @@ "application/validate": {"deployment", "failure", "repository", "selection"}, "application/evaluation": {"deployment", "failure", "reconcile", "repository", "secrets", "selection", "state"}, "application/inspect": {"application/evaluation", "deployment", "diff", "failure", "reconcile", "repository", "secrets", "selection", "state"}, - "application/apply": {"application/evaluation", "deployment", "diff", "failure", "filesystem", "hooks", "pathsafe", "reconcile", "repository", "secrets", "selection", "state"}, - "application/add": {"deployment", "failure", "filesystem", "pathsafe", "reconcile", "repository", "secrets", "selection", "state"}, + "application/apply": {"application/evaluation", "application/outcome", "deployment", "diff", "failure", "filesystem", "hooks", "pathsafe", "reconcile", "repository", "secrets", "selection", "state"}, + "application/add": {"application/outcome", "deployment", "failure", "filesystem", "pathsafe", "reconcile", "repository", "secrets", "selection", "state"}, + "application/outcome": {}, "application/version": {"buildinfo"}, "bootstrap": {"application/initialize", "application/validate", "application/inspect", "application/apply", "application/add", "application/version", "cli", "deployment", "failure", "filesystem", "hooks", "repository", "secrets", "selection", "state"}, "cmd/cattery": {"bootstrap", "cli", "failure"}, diff --git a/internal/state/repositories.go b/internal/state/repositories.go --- a/internal/state/repositories.go +++ b/internal/state/repositories.go @@ -207,7 +207,7 @@ // execBatch executes named statements in order, stopping at the first failure. func execBatch(transaction *sql.Tx, steps []sqlStep) error { for _, step := range steps { - if err := execNamed(transaction, step.operation, step.statement, step.arguments...); err != nil { + if err := execNamed(transaction, step); err != nil { return err } } @@ -216,10 +216,10 @@ // execNamed executes one named statement in the transaction, rolling back and // wrapping the error when the statement fails. -func execNamed(transaction *sql.Tx, operation, statement string, arguments ...any) error { - if _, err := transaction.Exec(statement, arguments...); err != nil { +func execNamed(transaction *sql.Tx, step sqlStep) error { + if _, err := transaction.Exec(step.statement, step.arguments...); err != nil { _ = transaction.Rollback() - return fmt.Errorf("state: %s: %w", operation, err) + return fmt.Errorf("state: %s: %w", step.operation, err) } return nil } @@ -227,7 +227,7 @@ // execIn executes one statement in the transaction, rolling back and wrapping // the error when the statement fails. func execIn(transaction *sql.Tx, statement string, arguments ...any) error { - return execNamed(transaction, statement, statement, arguments...) + return execNamed(transaction, sqlStep{operation: statement, statement: statement, arguments: arguments}) } // canonicalRepositoryPair resolves both paths to canonical absolute form so diff --git a/internal/application/add/types.go b/internal/application/add/types.go --- a/internal/application/add/types.go +++ b/internal/application/add/types.go @@ -11,6 +11,7 @@ "fmt" "io/fs" + "github.com/alyraffauf/cattery/internal/application/outcome" "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/filesystem" "github.com/alyraffauf/cattery/internal/repository" @@ -238,15 +239,12 @@ } // ItemStatus marks the outcome of one per-target add record. -type ItemStatus string +type ItemStatus = outcome.ItemStatus const ( - // StatusPlanned marks a dry-run or not-yet-executed record. - StatusPlanned ItemStatus = "planned" - // StatusCompleted marks a fully adopted target with an established baseline. - StatusCompleted ItemStatus = "completed" - // StatusPartial marks a source update kept without an equal baseline. - StatusPartial ItemStatus = "partial" + StatusPlanned = outcome.StatusPlanned + StatusCompleted = outcome.StatusCompleted + StatusPartial = outcome.StatusPartial ) // ItemResult is one per-target add record: the HOME-relative target, the @@ -259,11 +257,7 @@ } // Summary counts the per-target outcome records of one add. -type Summary struct { - Planned int - Completed int - Partial int -} +type Summary = outcome.Summary // Result is the frozen outcome of one add: the target-sorted per-target // records and the outcome counts. diff --git a/internal/application/apply/types.go b/internal/application/apply/types.go --- a/internal/application/apply/types.go +++ b/internal/application/apply/types.go @@ -10,6 +10,7 @@ "fmt" "github.com/alyraffauf/cattery/internal/application/evaluation" + "github.com/alyraffauf/cattery/internal/application/outcome" "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/filesystem" "github.com/alyraffauf/cattery/internal/hooks" @@ -287,15 +288,12 @@ } // ItemStatus marks the outcome of one per-target apply record. -type ItemStatus string +type ItemStatus = outcome.ItemStatus const ( - // StatusPlanned marks a dry-run or not-yet-executed record. - StatusPlanned ItemStatus = "planned" - // StatusCompleted marks a durable target with an equal baseline. - StatusCompleted ItemStatus = "completed" - // StatusPartial marks a durable target without an equal baseline. - StatusPartial ItemStatus = "partial" + StatusPlanned = outcome.StatusPlanned + StatusCompleted = outcome.StatusCompleted + StatusPartial = outcome.StatusPartial ) // ItemResult is one per-target apply record: the HOME-relative target, the @@ -308,11 +306,7 @@ } // Summary counts the per-target outcome records of one apply. -type Summary struct { - Planned int - Completed int - Partial int -} +type Summary = outcome.Summary // Result is the frozen outcome of one apply: the target-sorted per-target // records and the outcome counts. diff --git a/internal/application/apply/types_test.go b/internal/application/apply/types_test.go --- a/internal/application/apply/types_test.go +++ b/internal/application/apply/types_test.go @@ -311,7 +311,7 @@ reflected := reflect.TypeOf(value) for index := 0; index < reflected.NumField(); index++ { field := reflected.Field(index) - if field.Type.PkgPath() != "" && !strings.HasSuffix(field.Type.PkgPath(), "/internal/application/apply") { + if field.Type.PkgPath() != "" && !strings.Contains(field.Type.PkgPath(), "/internal/application/") { return true } } diff --git a/internal/application/inspect/diff.go b/internal/application/inspect/diff.go --- a/internal/application/inspect/diff.go +++ b/internal/application/inspect/diff.go @@ -49,30 +49,15 @@ } } -// parseDiffTag maps one stable tag name to its diff value. +// parseDiffTag preserves the application boundary while delegating the tag +// vocabulary to the diff package. func parseDiffTag(name string) diff.Tag { - switch name { - case "text": - return diff.TagText - case "binary": - return diff.TagBinary - case "secret": - return diff.TagSecret - } - return diff.TagNone + return diff.ParseTag(name) } // DiffTagName returns the stable lowercase name of one record's safe tag. func DiffTagName(record DiffRecord) string { - switch record.safe.Tag() { - case diff.TagText: - return "text" - case diff.TagBinary: - return "binary" - case diff.TagSecret: - return "secret" - } - return "none" + return record.safe.Tag().String() } func (record DiffRecord) TargetPath() string { return record.status.TargetPath() } diff --git a/internal/application/outcome/types.go b/internal/application/outcome/types.go new file mode 100644 --- /dev/null +++ b/internal/application/outcome/types.go @@ -0,0 +1,22 @@ +// Package outcome owns the shared per-target result vocabulary used by +// mutation commands. Command-specific item records remain in their owners. +package outcome + +// ItemStatus marks the outcome of one per-target command record. +type ItemStatus string + +const ( + // StatusPlanned marks a dry-run or not-yet-executed record. + StatusPlanned ItemStatus = "planned" + // StatusCompleted marks a durable record with an established baseline. + StatusCompleted ItemStatus = "completed" + // StatusPartial marks a record kept without an equal baseline. + StatusPartial ItemStatus = "partial" +) + +// Summary counts the per-target outcome records of one command. +type Summary struct { + Planned int + Completed int + Partial int +} diff --git a/internal/application/validate/service.go b/internal/application/validate/service.go --- a/internal/application/validate/service.go +++ b/internal/application/validate/service.go @@ -121,7 +121,7 @@ Selected: selected, }) if err != nil { - return deployment.Plan{}, compileFailure("validate: compile plan", err) + return deployment.Plan{}, failure.FromPathError("validate: compile plan", err) } return plan, nil } diff --git a/internal/application/validate/types.go b/internal/application/validate/types.go --- a/internal/application/validate/types.go +++ b/internal/application/validate/types.go @@ -7,22 +7,10 @@ package validate import ( - "errors" - "os" - "github.com/alyraffauf/cattery/internal/deployment" - "github.com/alyraffauf/cattery/internal/failure" "github.com/alyraffauf/cattery/internal/repository" "github.com/alyraffauf/cattery/internal/selection" ) - -func compileFailure(message string, cause error) error { - var pathError *os.PathError - if errors.As(cause, &pathError) { - return failure.New(failure.Operational, message, cause) - } - return failure.New(failure.InvalidInput, message, cause) -} // Dependencies bundles the injectable seams of the validation service. // RepositorySource resolves the canonical repository pair for a raw request; -- tangled.sh