Skip to content

Commit 7782163

Browse files
[Autoloop: python-to-go-migration] Iteration 43: migrate policy/helptext, policy/outcomerouting, primitives/primparser, output/scriptformatters
Run: https://github.com/githubnext/apm/actions/runs/25865196805 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dbd0669 commit 7782163

8 files changed

Lines changed: 1448 additions & 770 deletions

File tree

benchmarks/migration-status.json

Lines changed: 785 additions & 770 deletions
Large diffs are not rendered by default.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Package scriptformatters provides ASCII-only CLI output formatters for
2+
// APM script execution.
3+
// Migrated from src/apm_cli/output/script_formatters.py.
4+
// Rich/colour output is omitted -- all output is plain ASCII.
5+
package scriptformatters
6+
7+
import (
8+
"fmt"
9+
"strings"
10+
)
11+
12+
// ScriptExecutionFormatter formats script execution output as plain ASCII lines.
13+
type ScriptExecutionFormatter struct{}
14+
15+
// NewScriptExecutionFormatter returns a new formatter.
16+
func NewScriptExecutionFormatter() *ScriptExecutionFormatter {
17+
return &ScriptExecutionFormatter{}
18+
}
19+
20+
// FormatScriptHeader formats the script execution header with parameters.
21+
func (f *ScriptExecutionFormatter) FormatScriptHeader(scriptName string, params map[string]string) []string {
22+
lines := []string{fmt.Sprintf("[>] Running script: %s", scriptName)}
23+
for k, v := range params {
24+
lines = append(lines, fmt.Sprintf(" - %s: %s", k, v))
25+
}
26+
return lines
27+
}
28+
29+
// FormatCompilationProgress formats prompt compilation progress.
30+
func (f *ScriptExecutionFormatter) FormatCompilationProgress(promptFiles []string) []string {
31+
if len(promptFiles) == 0 {
32+
return nil
33+
}
34+
var lines []string
35+
if len(promptFiles) == 1 {
36+
lines = append(lines, "Compiling prompt...")
37+
} else {
38+
lines = append(lines, fmt.Sprintf("Compiling %d prompts...", len(promptFiles)))
39+
}
40+
for _, pf := range promptFiles {
41+
lines = append(lines, fmt.Sprintf("|- %s", pf))
42+
}
43+
if len(lines) > 1 {
44+
lines[len(lines)-1] = strings.Replace(lines[len(lines)-1], "|-", "+-", 1)
45+
}
46+
return lines
47+
}
48+
49+
// FormatRuntimeExecution formats runtime command execution details.
50+
func (f *ScriptExecutionFormatter) FormatRuntimeExecution(runtime, command string, contentLength int) []string {
51+
return []string{
52+
fmt.Sprintf("Executing %s runtime...", runtime),
53+
fmt.Sprintf("|- Command: %s", command),
54+
fmt.Sprintf("+- Prompt content: %d characters", contentLength),
55+
}
56+
}
57+
58+
// FormatContentPreview formats a content preview (plain text, no rich boxes).
59+
func (f *ScriptExecutionFormatter) FormatContentPreview(content string, maxPreview int) []string {
60+
if maxPreview <= 0 {
61+
maxPreview = 200
62+
}
63+
preview := content
64+
if len(content) > maxPreview {
65+
preview = content[:maxPreview] + "..."
66+
}
67+
return []string{
68+
"Prompt preview:",
69+
strings.Repeat("-", 50),
70+
preview,
71+
strings.Repeat("-", 50),
72+
}
73+
}
74+
75+
// FormatEnvironmentSetup formats environment setup information.
76+
func (f *ScriptExecutionFormatter) FormatEnvironmentSetup(runtime string, envVarsSet []string) []string {
77+
if len(envVarsSet) == 0 {
78+
return nil
79+
}
80+
lines := []string{"Environment setup:"}
81+
for _, v := range envVarsSet {
82+
lines = append(lines, fmt.Sprintf("|- %s: configured", v))
83+
}
84+
if len(lines) > 1 {
85+
lines[len(lines)-1] = strings.Replace(lines[len(lines)-1], "|-", "+-", 1)
86+
}
87+
return lines
88+
}
89+
90+
// FormatExecutionSuccess formats a successful execution result.
91+
// executionTime < 0 means not provided.
92+
func (f *ScriptExecutionFormatter) FormatExecutionSuccess(runtime string, executionTime float64) []string {
93+
msg := fmt.Sprintf("[+] %s execution completed successfully", titleCase(runtime))
94+
if executionTime >= 0 {
95+
msg += fmt.Sprintf(" (%.2fs)", executionTime)
96+
}
97+
return []string{msg}
98+
}
99+
100+
// FormatExecutionError formats an execution error result.
101+
func (f *ScriptExecutionFormatter) FormatExecutionError(runtime string, errorCode int, errorMsg string) []string {
102+
lines := []string{
103+
fmt.Sprintf("x %s execution failed (exit code: %d)", titleCase(runtime), errorCode),
104+
}
105+
if errorMsg != "" {
106+
for _, line := range strings.Split(errorMsg, "\n") {
107+
if strings.TrimSpace(line) != "" {
108+
lines = append(lines, " "+line)
109+
}
110+
}
111+
}
112+
return lines
113+
}
114+
115+
// FormatSubprocessDetails formats subprocess execution details.
116+
func (f *ScriptExecutionFormatter) FormatSubprocessDetails(args []string, contentLength int) []string {
117+
quoted := make([]string, len(args))
118+
for i, a := range args {
119+
if strings.Contains(a, " ") {
120+
quoted[i] = `"` + a + `"`
121+
} else {
122+
quoted[i] = a
123+
}
124+
}
125+
return []string{
126+
"Subprocess execution:",
127+
fmt.Sprintf("|- Args: %s", strings.Join(quoted, " ")),
128+
fmt.Sprintf("+- Content: +%d chars appended", contentLength),
129+
}
130+
}
131+
132+
// FormatAutoDiscoveryMessage formats the message for auto-discovered prompts.
133+
func (f *ScriptExecutionFormatter) FormatAutoDiscoveryMessage(scriptName, promptFile, runtime string) string {
134+
return fmt.Sprintf("[i] Auto-discovered: %s (runtime: %s)", promptFile, runtime)
135+
}
136+
137+
// titleCase capitalises the first rune of s.
138+
func titleCase(s string) string {
139+
if s == "" {
140+
return s
141+
}
142+
return strings.ToUpper(s[:1]) + s[1:]
143+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Package helptext contains shared help text for policy-related CLI commands.
2+
// Migrated from src/apm_cli/policy/_help_text.py.
3+
package helptext
4+
5+
// PolicySourceFormsHelp is the canonical user-facing description of the
6+
// --policy / --policy-source argument formats accepted by discover_policy.
7+
const PolicySourceFormsHelp = "Accepts: 'org' (auto-discover from your project's git remote), " +
8+
"'owner/repo' (defaults to github.com), an https:// URL, or a " +
9+
"local file path."
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Package outcomerouting is the single source of truth for the 9-outcome
2+
// policy-discovery routing table.
3+
// Migrated from src/apm_cli/policy/outcome_routing.py.
4+
package outcomerouting
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/githubnext/apm/internal/policy/schema"
10+
)
11+
12+
// PolicyViolationError is raised when a policy demands fail-closed behaviour.
13+
type PolicyViolationError struct {
14+
Message string
15+
PolicySource string
16+
}
17+
18+
func (e *PolicyViolationError) Error() string {
19+
return e.Message
20+
}
21+
22+
// PolicyFetchResult holds the result of a discover_policy call.
23+
type PolicyFetchResult struct {
24+
Outcome string
25+
Source string
26+
Cached bool
27+
Error string
28+
FetchError string
29+
CacheAgeSeconds int
30+
Policy *schema.ApmPolicy
31+
}
32+
33+
// PolicyLogger is the minimal interface expected of a logger for routing.
34+
type PolicyLogger interface {
35+
PolicyResolved(source string, cached bool, enforcement string, ageSeconds int)
36+
PolicyDiscoveryMiss(outcome string, source string, err string)
37+
}
38+
39+
// outcomesHonoringFetchFailureDefault is the set of outcomes that respect the
40+
// project-side policy.fetch_failure_default knob.
41+
var outcomesHonoringFetchFailureDefault = map[string]bool{
42+
"malformed": true,
43+
"cache_miss_fetch_fail": true,
44+
"garbage_response": true,
45+
"no_git_remote": true,
46+
"absent": true,
47+
"empty": true,
48+
}
49+
50+
// nonFoundLoggedOutcomes is the set of outcomes routed through the canonical
51+
// policy_discovery_miss logger helper.
52+
var nonFoundLoggedOutcomes = map[string]bool{
53+
"absent": true,
54+
"no_git_remote": true,
55+
"empty": true,
56+
"malformed": true,
57+
"cache_miss_fetch_fail": true,
58+
"garbage_response": true,
59+
}
60+
61+
// RouteDiscoveryOutcome routes a PolicyFetchResult to logging and fail-closed
62+
// decisions.
63+
//
64+
// Parameters:
65+
// - fetchResult: result of discover_policy_with_chain
66+
// - logger: logger implementing PolicyLogger (nil is tolerated)
67+
// - fetchFailureDefault: project-side policy.fetch_failure_default ("warn" or "block")
68+
// - raiseBlockingErrors: when true, return a PolicyViolationError for blocking outcomes
69+
//
70+
// Returns the effective ApmPolicy when enforcement should proceed, nil otherwise.
71+
// When raiseBlockingErrors is true and a blocking condition is met, a non-nil error
72+
// is returned alongside a nil policy.
73+
func RouteDiscoveryOutcome(
74+
fetchResult PolicyFetchResult,
75+
logger PolicyLogger,
76+
fetchFailureDefault string,
77+
raiseBlockingErrors bool,
78+
) (*schema.ApmPolicy, error) {
79+
outcome := fetchResult.Outcome
80+
source := fetchResult.Source
81+
82+
if outcome == "disabled" {
83+
return nil, nil
84+
}
85+
86+
// hash_mismatch: ALWAYS fail closed regardless of fetch_failure_default.
87+
if outcome == "hash_mismatch" {
88+
errStr := fetchResult.Error
89+
if errStr == "" {
90+
errStr = fetchResult.FetchError
91+
}
92+
if logger != nil {
93+
logger.PolicyDiscoveryMiss("hash_mismatch", source, errStr)
94+
}
95+
if raiseBlockingErrors {
96+
return nil, &PolicyViolationError{
97+
Message: fmt.Sprintf(
98+
"Install blocked: policy hash mismatch -- pinned policy.hash "+
99+
"does not match fetched policy bytes (source=%s). "+
100+
"Update apm.yml policy.hash or contact your org admin.",
101+
sourceOrUnknown(source),
102+
),
103+
PolicySource: sourceOrUnknown(source),
104+
}
105+
}
106+
return nil, nil
107+
}
108+
109+
// 6 of 9 non-found outcomes route through the canonical logger helper.
110+
if nonFoundLoggedOutcomes[outcome] {
111+
errStr := fetchResult.Error
112+
if errStr == "" {
113+
errStr = fetchResult.FetchError
114+
}
115+
if logger != nil {
116+
logger.PolicyDiscoveryMiss(outcome, source, errStr)
117+
}
118+
if raiseBlockingErrors &&
119+
outcomesHonoringFetchFailureDefault[outcome] &&
120+
fetchFailureDefault == "block" {
121+
return nil, &PolicyViolationError{
122+
Message: fmt.Sprintf(
123+
"Install blocked: no enforceable org policy was resolved "+
124+
"(outcome=%s) and project apm.yml has "+
125+
"policy.fetch_failure_default=block (source=%s)",
126+
outcome,
127+
sourceOrUnknown(source),
128+
),
129+
PolicySource: sourceOrUnknown(source),
130+
}
131+
}
132+
return nil, nil
133+
}
134+
135+
// cached_stale: log, enforce with the cached policy, potentially fail closed.
136+
if outcome == "cached_stale" {
137+
policy := fetchResult.Policy
138+
if logger != nil {
139+
if policy != nil {
140+
enforcement := policy.Enforcement
141+
if enforcement == "" {
142+
enforcement = "warn"
143+
}
144+
logger.PolicyResolved(source, true, enforcement, fetchResult.CacheAgeSeconds)
145+
}
146+
logger.PolicyDiscoveryMiss("cached_stale", source, fetchResult.FetchError)
147+
}
148+
if raiseBlockingErrors && policy != nil {
149+
ff := policy.FetchFailure
150+
if ff == "" {
151+
ff = "warn"
152+
}
153+
if ff == "block" {
154+
return nil, &PolicyViolationError{
155+
Message: fmt.Sprintf(
156+
"Install blocked: org policy refresh failed and the cached "+
157+
"policy declares fetch_failure=block (source=%s)",
158+
sourceOrUnknown(source),
159+
),
160+
PolicySource: sourceOrUnknown(source),
161+
}
162+
}
163+
}
164+
return policy, nil
165+
}
166+
167+
// found: normal path
168+
if outcome == "found" {
169+
policy := fetchResult.Policy
170+
if logger != nil && policy != nil {
171+
enforcement := policy.Enforcement
172+
if enforcement == "" {
173+
enforcement = "warn"
174+
}
175+
logger.PolicyResolved(source, fetchResult.Cached, enforcement, fetchResult.CacheAgeSeconds)
176+
}
177+
return policy, nil
178+
}
179+
180+
// Defensive: unrecognised outcome -- skip enforcement.
181+
return nil, nil
182+
}
183+
184+
func sourceOrUnknown(s string) string {
185+
if s == "" {
186+
return "unknown"
187+
}
188+
return s
189+
}

internal/policy/schema/schema.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Cache PolicyCache
5454
Deps DependencyPolicy
5555
MCP McpPolicy
5656
Compilation CompilationPolicy
57+
Enforcement string // warn | block | off (default: warn)
58+
FetchFailure string // warn | block (default: warn)
5759
}
5860

5961
// DefaultDependencyPolicy returns a DependencyPolicy with sensible defaults.

internal/primitives/primmodels/primmodels.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// Package primmodels defines data models for APM primitives.
22
package primmodels
33

4+
// Primitive is the common interface for all APM primitive types.
5+
type Primitive interface {
6+
Validate() []string
7+
}
8+
49
// Chatmode represents a chatmode primitive.
510
type Chatmode struct {
611
Name string
@@ -60,6 +65,14 @@ Version string
6065
Source string
6166
}
6267

68+
// Validate returns validation errors for a Context.
69+
func (c *Context) Validate() []string {
70+
if c.Content == "" {
71+
return []string{"Empty content"}
72+
}
73+
return nil
74+
}
75+
6376
// Skill represents a skill primitive.
6477
type Skill struct {
6578
Name string
@@ -72,6 +85,11 @@ Version string
7285
Source string
7386
}
7487

88+
// Validate returns validation errors for a Skill.
89+
func (s *Skill) Validate() []string {
90+
return nil
91+
}
92+
7593
// Agent represents an agent primitive.
7694
type Agent struct {
7795
Name string

0 commit comments

Comments
 (0)