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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,18 @@ gitcontribute validation define --kind=test --command="go test ./..." \
--working-dir=/path/to/workspace <investigation-id>
gitcontribute validation run <validation-id> --kind=base --execute
gitcontribute validation run <validation-id> --kind=candidate --execute
gitcontribute validation repeat <validation-id> --kind=both --runs=5 \
--concurrency=2 --execute
gitcontribute validation compare <base-run-id> <candidate-run-id>
```

Repeat validation stores bounded per-attempt results plus stable/flaky/
inconclusive aggregates. CPU time, peak RSS, child count, process identity, and
post-shutdown cleanup are sampled through `gopsutil`; unsupported metrics stay
explicitly unavailable. To measure MCP readiness without parsing stdout, define
the validation with `--protocol=mcp_stdio --readiness-timeout=30s`. The official
MCP Go SDK then records initialize and tools/list milestones.

### 4. Check readiness

Run a deterministic local readiness gate before turning a candidate into a
Expand Down
11 changes: 10 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,21 @@ application and domain packages expose product-owned values and interfaces.
| DeepWiki external read | public repository structure, contents, questions | yes | no | no | no |
| Git acquisition | acquire, workspace create | remote-dependent | yes | `git` only | no |
| Local merge check | compare already-fetched revisions | no | no | `git` only | no |
| Validation | validation run with explicit execution | no by default | yes | yes | no |
| Validation | validation run/repeat with explicit execution | no by default | yes | yes | no |

Version 1 has no GitHub mutation path. Adding one requires a separate
application capability and protocol annotation; it must not be hidden behind a
read operation.

Validation definitions remain shell-free and execution requires an explicit
authorization flag. Repeat groups bound attempts, concurrency, per-run and
overall timeouts, captured output, and sampler frequency. The generic process
runner records spawn/execution/shutdown boundaries and uses `gopsutil` for
PID-plus-start-time process-tree metrics. Declared `mcp_stdio` definitions use
the official MCP SDK for initialize and tools/list milestones; protocol state
is never inferred from arbitrary stdout text. Metric availability and cleanup
status are persisted separately from semantic pass/fail classification.

## Corpus model

The corpus separates source history from convenient current state:
Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/modelcontextprotocol/go-sdk v1.6.1
github.com/pelletier/go-toml/v2 v2.4.3
github.com/pressly/goose/v3 v3.24.0
github.com/shirou/gopsutil/v4 v4.26.6
github.com/sourcegraph/go-diff v0.8.0
github.com/zalando/go-keyring v0.2.8
golang.org/x/mod v0.37.0
Expand Down Expand Up @@ -45,10 +46,13 @@ require (
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
Expand All @@ -58,13 +62,17 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.21.0 // indirect
Expand Down
20 changes: 20 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
Expand All @@ -95,6 +100,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
Expand All @@ -119,6 +126,8 @@ github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdD
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pressly/goose/v3 v3.24.0 h1:sFbNms7Bd++2VMq6HSgDHDLWa7kHz1qXzPb3ZIU72VU=
github.com/pressly/goose/v3 v3.24.0/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
Expand All @@ -131,16 +140,24 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQytHhvY=
github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
Expand All @@ -153,6 +170,8 @@ golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
Expand All @@ -163,6 +182,7 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
Expand Down
101 changes: 101 additions & 0 deletions internal/app/evidence.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func (s *Service) DefineValidation(ctx context.Context, investigationID string,
Timeout: opts.Timeout,
MaxOutputBytes: opts.MaxOutputBytes,
Observation: observationContractToEvidence(opts.Observation),
Protocol: evidence.ValidationProtocol(opts.Protocol),
ReadinessTimeout: opts.ReadinessTimeout,
}

evSvc := evidence.NewService(c, evidence.NewExecRunner())
Expand Down Expand Up @@ -223,6 +225,29 @@ func bindValidationWorkspace(ctx context.Context, service *Service, c *corpus.Co
return nil
}

// RunValidationGroup executes a bounded repeat/stress validation group.
func (s *Service) RunValidationGroup(ctx context.Context, id string, opts cli.RepeatValidationOptions) (*cli.ValidationRunGroupResult, error) {
if !opts.Execute {
return nil, evidence.ErrExecutionNotAuthorized
}
kinds := make([]evidence.RunKind, len(opts.Kinds))
for index, kind := range opts.Kinds {
kinds[index] = evidence.RunKind(kind)
}
c, err := s.openCorpus(ctx)
if err != nil {
return nil, err
}
group, err := evidence.NewService(c, evidence.NewExecRunner()).RunValidationGroup(ctx, id, evidence.RepeatValidationOptions{
Kinds: kinds, RunCount: opts.RunCount, Concurrency: opts.Concurrency,
PerRunTimeout: opts.PerRunTimeout, OverallTimeout: opts.OverallTimeout, SampleInterval: opts.SampleInterval,
})
if err != nil {
return nil, mapEvidenceError(err)
}
return validationRunGroupResult(group), nil
}

// CompareValidation compares a base validation run with a candidate validation run.
func (s *Service) CompareValidation(ctx context.Context, baseRunID, candidateRunID string) (*cli.ValidationComparisonResult, error) {
c, err := s.openReadOnlyCorpus(ctx)
Expand Down Expand Up @@ -363,6 +388,10 @@ func validationResult(def *evidence.ValidationDefinition) *cli.ValidationResult
if def.Timeout > 0 {
timeout = def.Timeout.String()
}
readinessTimeout := ""
if def.ReadinessTimeout > 0 {
readinessTimeout = def.ReadinessTimeout.String()
}
return &cli.ValidationResult{
ID: def.ID,
InvestigationID: def.InvestigationID,
Expand All @@ -378,6 +407,8 @@ func validationResult(def *evidence.ValidationDefinition) *cli.ValidationResult
Timeout: timeout,
MaxOutputBytes: def.MaxOutputBytes,
Observation: observationContractToCLI(def.Observation),
Protocol: string(def.Protocol),
ReadinessTimeout: readinessTimeout,
CreatedAt: formatTime(def.CreatedAt),
}
}
Expand All @@ -402,7 +433,77 @@ func validationRunResult(run *evidence.ValidationRun) *cli.ValidationRunResult {
WorkspaceSnapshotAfter: run.WorkspaceSnapshotAfter,
WorkspaceBindingStatus: run.WorkspaceBindingStatus,
WorkspaceBindingReason: run.WorkspaceBindingReason,
Process: validationProcessIdentity(run.Process),
Phases: validationPhases(run.Phases),
TimeoutPhase: run.TimeoutPhase,
FailurePhase: run.FailurePhase,
Resources: validationResources(run.Resources),
Cleanup: validationCleanup(run.Cleanup),
}
}

func validationRunGroupResult(group *evidence.ValidationRunGroup) *cli.ValidationRunGroupResult {
result := &cli.ValidationRunGroupResult{
ID: group.ID, DefinitionID: group.DefinitionID, InvestigationID: group.InvestigationID,
ConfigurationSHA256: group.ConfigurationSHA256, RequestedRuns: group.RequestedRuns, CompletedRuns: group.CompletedRuns,
Concurrency: group.Concurrency, PerRunTimeout: group.PerRunTimeout.String(), OverallTimeout: group.OverallTimeout.String(),
SampleInterval: group.SampleInterval.String(), Classification: string(group.Classification),
StartedAt: formatTime(group.StartedAt), CompletedAt: formatTime(group.CompletedAt),
}
for _, attempt := range group.Attempts {
result.Attempts = append(result.Attempts, cli.ValidationAttemptResult{
Index: attempt.Index, Kind: string(attempt.Kind), RunID: attempt.RunID,
StartedAt: formatTime(attempt.StartedAt), CompletedAt: formatTime(attempt.CompletedAt), ExitCode: attempt.ExitCode,
Classification: string(attempt.Classification), ObservationStatus: string(attempt.ObservationStatus),
TimeoutPhase: attempt.TimeoutPhase, FailurePhase: attempt.FailurePhase,
Error: attempt.Error, Process: validationProcessIdentity(attempt.Process),
Phases: validationPhases(attempt.Phases),
Resources: validationResources(attempt.Resources), Cleanup: validationCleanup(attempt.Cleanup),
})
}
for _, aggregate := range group.Aggregates {
result.Aggregates = append(result.Aggregates, cli.ValidationAggregateResult{
Kind: string(aggregate.Kind), Requested: aggregate.Requested, Completed: aggregate.Completed,
Passing: aggregate.Passing, Failing: aggregate.Failing, Inconclusive: aggregate.Inconclusive,
Cancelled: aggregate.Cancelled, Classification: string(aggregate.Classification),
ResourceClassification: aggregate.ResourceClassification,
})
}
if group.Comparison != nil {
result.Comparison = &cli.ValidationGroupComparisonResult{Classification: string(group.Comparison.Classification), Explanation: group.Comparison.Explanation}
}
return result
}

func validationPhases(value evidence.RunPhases) cli.ValidationRunPhases {
return cli.ValidationRunPhases{
SpawnStartedAt: formatTime(value.SpawnStartedAt), ProcessStartedAt: formatTime(value.ProcessStartedAt),
InitializedAt: formatTime(value.InitializedAt), ToolsListedAt: formatTime(value.ToolsListedAt),
FirstResponseAt: formatTime(value.FirstResponseAt), ExecutionEndedAt: formatTime(value.ExecutionEndedAt),
ShutdownStartedAt: formatTime(value.ShutdownStartedAt), ShutdownCheckedAt: formatTime(value.ShutdownCheckedAt),
}
}

func validationProcessIdentity(value evidence.ProcessIdentity) cli.ValidationProcessIdentity {
return cli.ValidationProcessIdentity{PID: value.PID, CreateTimeUnixMilli: value.CreateTimeUnixMilli}
}

func validationResources(value evidence.ResourceTelemetry) cli.ValidationResourceTelemetry {
return cli.ValidationResourceTelemetry{
Provider: value.Provider, Platform: value.Platform, SampleInterval: value.SampleInterval.String(), SampleCount: value.SampleCount,
CPUTimeMillis: cli.ValidationInt64Metric{Value: value.CPUTimeMillis.Value, UnavailableReason: value.CPUTimeMillis.UnavailableReason},
PeakRSSBytes: cli.ValidationUint64Metric{Value: value.PeakRSSBytes.Value, UnavailableReason: value.PeakRSSBytes.UnavailableReason},
PeakChildCount: cli.ValidationInt64Metric{Value: value.PeakChildCount.Value, UnavailableReason: value.PeakChildCount.UnavailableReason},
SamplerOverheadNanoseconds: value.SamplerOverheadNanoseconds,
}
}

func validationCleanup(value evidence.CleanupResult) cli.ValidationCleanupResult {
result := cli.ValidationCleanupResult{Status: value.Status, Reason: value.Reason, CheckedAt: formatTime(value.CheckedAt)}
for _, survivor := range value.Survivors {
result.Survivors = append(result.Survivors, validationProcessIdentity(survivor))
}
return result
}

func observationContractToEvidence(contract *cli.ValidationObservationContract) *evidence.ObservationContract {
Expand Down
12 changes: 12 additions & 0 deletions internal/app/mcp_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,14 @@ func (r *MCPReader) DefineValidation(ctx context.Context, in mcpserver.DefineVal
}
timeout = d
}
var readinessTimeout time.Duration
if in.ReadinessTimeout != "" {
d, err := time.ParseDuration(in.ReadinessTimeout)
if err != nil {
return mcpserver.ValidationOutput{}, fmt.Errorf("invalid readiness timeout: %w", err)
}
readinessTimeout = d
}
opts := cli.DefineValidationOptions{
Kind: in.Kind,
Command: in.Command,
Expand All @@ -608,6 +616,8 @@ func (r *MCPReader) DefineValidation(ctx context.Context, in mcpserver.DefineVal
Timeout: timeout,
MaxOutputBytes: in.MaxOutputBytes,
Observation: observationContractMCPToCLI(in.Observation),
Protocol: in.Protocol,
ReadinessTimeout: readinessTimeout,
}
res, err := r.Service.DefineValidation(ctx, in.InvestigationID, opts)
if err != nil {
Expand All @@ -632,6 +642,8 @@ func validationResultToMCP(res *cli.ValidationResult) mcpserver.ValidationOutput
Timeout: res.Timeout,
MaxOutputBytes: res.MaxOutputBytes,
Observation: observationContractCLIToMCP(res.Observation),
Protocol: res.Protocol,
ReadinessTimeout: res.ReadinessTimeout,
CreatedAt: res.CreatedAt,
}
}
Expand Down
Loading
Loading