diff --git a/api/stovepipe/proto/stovepipe.proto b/api/stovepipe/proto/stovepipe.proto index a00decc9c..54ee2f83a 100644 --- a/api/stovepipe/proto/stovepipe.proto +++ b/api/stovepipe/proto/stovepipe.proto @@ -54,6 +54,57 @@ message IngestResponse { string id = 1; } +// ValidationResult is the immutable validation verdict for one scope. +message ValidationResult { + // Degree measures how broken the scope is on [0.0, 1.0]. Zero is fully + // green and one is fully broken. + double degree = 1; +} + +// GetProjectStatusByURIRequest selects the authoritative validation for an exact commit URI. +message GetProjectStatusByURIRequest { + // Logical queue containing the request. + string queue = 1; + // Exact VCS-agnostic commit URI whose authoritative request is selected. + string change_uri = 2; + // Optional exact project scope. When absent, one page of planned projects is returned. + optional string project = 3; + // Maximum projects to return. Zero selects the server default. + int32 page_size = 4; + // Opaque continuation token. Empty selects the first page. + string page_token = 5; +} + +// ProjectValidation is the recorded validation for one planned project. +message ProjectValidation { + // Stable project identifier. + string project = 1; + // Immutable verdict for the project. Unset until its fact is recorded. + ValidationResult result = 2; +} + +// GetProjectStatusByURIResponse contains the authoritative request's current validation projection. +message GetProjectStatusByURIResponse { + // Globally unique identifier of the authoritative request. + string request_id = 1; + // Logical queue containing the request. + string queue = 2; + // VCS-agnostic commit URI validated by the request. + string change_uri = 3; + // Baseline URI for incremental validation. Empty for a full build. + string base_uri = 4; + // Stable public lifecycle state of the request. + string request_state = 5; + // Immutable whole-repository verdict. Unset until its fact is recorded. + ValidationResult repository_result = 6; + // Whether every planned project has one durable result and completion is recorded. + bool project_results_complete = 7; + // Planned projects in stable project order, with any recorded verdicts. + repeated ProjectValidation projects = 8; + // Opaque continuation token. Empty on the final page. + string next_page_token = 9; +} + // Stovepipe provides the Stovepipe API. service Stovepipe { // Ping returns a response indicating the service is alive @@ -61,4 +112,6 @@ service Stovepipe { // Ingest admits a queue's newly observed commit into the validation pipeline and returns // the minted request ID. The caller hands off asynchronously; validation happens later. rpc Ingest(IngestRequest) returns (IngestResponse) {} + // GetProjectStatusByURI returns current validation for an exact commit URI's authoritative request. + rpc GetProjectStatusByURI(GetProjectStatusByURIRequest) returns (GetProjectStatusByURIResponse) {} } diff --git a/api/stovepipe/protopb/stovepipe.pb.go b/api/stovepipe/protopb/stovepipe.pb.go index 25c1a6821..2b40cbf7e 100644 --- a/api/stovepipe/protopb/stovepipe.pb.go +++ b/api/stovepipe/protopb/stovepipe.pb.go @@ -250,6 +250,308 @@ func (x *IngestResponse) GetId() string { return "" } +// ValidationResult is the immutable validation verdict for one scope. +type ValidationResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Degree measures how broken the scope is on [0.0, 1.0]. Zero is fully + // green and one is fully broken. + Degree float64 `protobuf:"fixed64,1,opt,name=degree,proto3" json:"degree,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidationResult) Reset() { + *x = ValidationResult{} + mi := &file_stovepipe_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidationResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidationResult) ProtoMessage() {} + +func (x *ValidationResult) ProtoReflect() protoreflect.Message { + mi := &file_stovepipe_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidationResult.ProtoReflect.Descriptor instead. +func (*ValidationResult) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{4} +} + +func (x *ValidationResult) GetDegree() float64 { + if x != nil { + return x.Degree + } + return 0 +} + +// GetProjectStatusByURIRequest selects the authoritative validation for an exact commit URI. +type GetProjectStatusByURIRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Logical queue containing the request. + Queue string `protobuf:"bytes,1,opt,name=queue,proto3" json:"queue,omitempty"` + // Exact VCS-agnostic commit URI whose authoritative request is selected. + ChangeUri string `protobuf:"bytes,2,opt,name=change_uri,json=changeUri,proto3" json:"change_uri,omitempty"` + // Optional exact project scope. When absent, one page of planned projects is returned. + Project *string `protobuf:"bytes,3,opt,name=project,proto3,oneof" json:"project,omitempty"` + // Maximum projects to return. Zero selects the server default. + PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque continuation token. Empty selects the first page. + PageToken string `protobuf:"bytes,5,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectStatusByURIRequest) Reset() { + *x = GetProjectStatusByURIRequest{} + mi := &file_stovepipe_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectStatusByURIRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectStatusByURIRequest) ProtoMessage() {} + +func (x *GetProjectStatusByURIRequest) ProtoReflect() protoreflect.Message { + mi := &file_stovepipe_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProjectStatusByURIRequest.ProtoReflect.Descriptor instead. +func (*GetProjectStatusByURIRequest) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{5} +} + +func (x *GetProjectStatusByURIRequest) GetQueue() string { + if x != nil { + return x.Queue + } + return "" +} + +func (x *GetProjectStatusByURIRequest) GetChangeUri() string { + if x != nil { + return x.ChangeUri + } + return "" +} + +func (x *GetProjectStatusByURIRequest) GetProject() string { + if x != nil && x.Project != nil { + return *x.Project + } + return "" +} + +func (x *GetProjectStatusByURIRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *GetProjectStatusByURIRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// ProjectValidation is the recorded validation for one planned project. +type ProjectValidation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable project identifier. + Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + // Immutable verdict for the project. Unset until its fact is recorded. + Result *ValidationResult `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProjectValidation) Reset() { + *x = ProjectValidation{} + mi := &file_stovepipe_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProjectValidation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectValidation) ProtoMessage() {} + +func (x *ProjectValidation) ProtoReflect() protoreflect.Message { + mi := &file_stovepipe_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProjectValidation.ProtoReflect.Descriptor instead. +func (*ProjectValidation) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{6} +} + +func (x *ProjectValidation) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +func (x *ProjectValidation) GetResult() *ValidationResult { + if x != nil { + return x.Result + } + return nil +} + +// GetProjectStatusByURIResponse contains the authoritative request's current validation projection. +type GetProjectStatusByURIResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Globally unique identifier of the authoritative request. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Logical queue containing the request. + Queue string `protobuf:"bytes,2,opt,name=queue,proto3" json:"queue,omitempty"` + // VCS-agnostic commit URI validated by the request. + ChangeUri string `protobuf:"bytes,3,opt,name=change_uri,json=changeUri,proto3" json:"change_uri,omitempty"` + // Baseline URI for incremental validation. Empty for a full build. + BaseUri string `protobuf:"bytes,4,opt,name=base_uri,json=baseUri,proto3" json:"base_uri,omitempty"` + // Stable public lifecycle state of the request. + RequestState string `protobuf:"bytes,5,opt,name=request_state,json=requestState,proto3" json:"request_state,omitempty"` + // Immutable whole-repository verdict. Unset until its fact is recorded. + RepositoryResult *ValidationResult `protobuf:"bytes,6,opt,name=repository_result,json=repositoryResult,proto3" json:"repository_result,omitempty"` + // Whether every planned project has one durable result and completion is recorded. + ProjectResultsComplete bool `protobuf:"varint,7,opt,name=project_results_complete,json=projectResultsComplete,proto3" json:"project_results_complete,omitempty"` + // Planned projects in stable project order, with any recorded verdicts. + Projects []*ProjectValidation `protobuf:"bytes,8,rep,name=projects,proto3" json:"projects,omitempty"` + // Opaque continuation token. Empty on the final page. + NextPageToken string `protobuf:"bytes,9,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectStatusByURIResponse) Reset() { + *x = GetProjectStatusByURIResponse{} + mi := &file_stovepipe_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectStatusByURIResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectStatusByURIResponse) ProtoMessage() {} + +func (x *GetProjectStatusByURIResponse) ProtoReflect() protoreflect.Message { + mi := &file_stovepipe_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProjectStatusByURIResponse.ProtoReflect.Descriptor instead. +func (*GetProjectStatusByURIResponse) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{7} +} + +func (x *GetProjectStatusByURIResponse) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetQueue() string { + if x != nil { + return x.Queue + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetChangeUri() string { + if x != nil { + return x.ChangeUri + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetBaseUri() string { + if x != nil { + return x.BaseUri + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetRequestState() string { + if x != nil { + return x.RequestState + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetRepositoryResult() *ValidationResult { + if x != nil { + return x.RepositoryResult + } + return nil +} + +func (x *GetProjectStatusByURIResponse) GetProjectResultsComplete() bool { + if x != nil { + return x.ProjectResultsComplete + } + return false +} + +func (x *GetProjectStatusByURIResponse) GetProjects() []*ProjectValidation { + if x != nil { + return x.Projects + } + return nil +} + +func (x *GetProjectStatusByURIResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + var File_stovepipe_proto protoreflect.FileDescriptor const file_stovepipe_proto_rawDesc = "" + @@ -265,10 +567,38 @@ const file_stovepipe_proto_rawDesc = "" + "\rIngestRequest\x12\x14\n" + "\x05queue\x18\x01 \x01(\tR\x05queue\" \n" + "\x0eIngestResponse\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id2\xcb\x01\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"*\n" + + "\x10ValidationResult\x12\x16\n" + + "\x06degree\x18\x01 \x01(\x01R\x06degree\"\xba\x01\n" + + "\x1cGetProjectStatusByURIRequest\x12\x14\n" + + "\x05queue\x18\x01 \x01(\tR\x05queue\x12\x1d\n" + + "\n" + + "change_uri\x18\x02 \x01(\tR\tchangeUri\x12\x1d\n" + + "\aproject\x18\x03 \x01(\tH\x00R\aproject\x88\x01\x01\x12\x1b\n" + + "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x05 \x01(\tR\tpageTokenB\n" + + "\n" + + "\b_project\"s\n" + + "\x11ProjectValidation\x12\x18\n" + + "\aproject\x18\x01 \x01(\tR\aproject\x12D\n" + + "\x06result\x18\x02 \x01(\v2,.uber.submitqueue.stovepipe.ValidationResultR\x06result\"\xbb\x03\n" + + "\x1dGetProjectStatusByURIResponse\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x14\n" + + "\x05queue\x18\x02 \x01(\tR\x05queue\x12\x1d\n" + + "\n" + + "change_uri\x18\x03 \x01(\tR\tchangeUri\x12\x19\n" + + "\bbase_uri\x18\x04 \x01(\tR\abaseUri\x12#\n" + + "\rrequest_state\x18\x05 \x01(\tR\frequestState\x12Y\n" + + "\x11repository_result\x18\x06 \x01(\v2,.uber.submitqueue.stovepipe.ValidationResultR\x10repositoryResult\x128\n" + + "\x18project_results_complete\x18\a \x01(\bR\x16projectResultsComplete\x12I\n" + + "\bprojects\x18\b \x03(\v2-.uber.submitqueue.stovepipe.ProjectValidationR\bprojects\x12&\n" + + "\x0fnext_page_token\x18\t \x01(\tR\rnextPageToken2\xdc\x02\n" + "\tStovepipe\x12[\n" + "\x04Ping\x12'.uber.submitqueue.stovepipe.PingRequest\x1a(.uber.submitqueue.stovepipe.PingResponse\"\x00\x12a\n" + - "\x06Ingest\x12).uber.submitqueue.stovepipe.IngestRequest\x1a*.uber.submitqueue.stovepipe.IngestResponse\"\x00Be\n" + + "\x06Ingest\x12).uber.submitqueue.stovepipe.IngestRequest\x1a*.uber.submitqueue.stovepipe.IngestResponse\"\x00\x12\x8e\x01\n" + + "\x15GetProjectStatusByURI\x128.uber.submitqueue.stovepipe.GetProjectStatusByURIRequest\x1a9.uber.submitqueue.stovepipe.GetProjectStatusByURIResponse\"\x00Be\n" + "\x1ecom.uber.submitqueue.stovepipeB\x0eStovepipeProtoP\x01Z1github.com/uber/submitqueue/api/stovepipe/protopbb\x06proto3" var ( @@ -283,23 +613,32 @@ func file_stovepipe_proto_rawDescGZIP() []byte { return file_stovepipe_proto_rawDescData } -var file_stovepipe_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_stovepipe_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_stovepipe_proto_goTypes = []any{ - (*PingRequest)(nil), // 0: uber.submitqueue.stovepipe.PingRequest - (*PingResponse)(nil), // 1: uber.submitqueue.stovepipe.PingResponse - (*IngestRequest)(nil), // 2: uber.submitqueue.stovepipe.IngestRequest - (*IngestResponse)(nil), // 3: uber.submitqueue.stovepipe.IngestResponse + (*PingRequest)(nil), // 0: uber.submitqueue.stovepipe.PingRequest + (*PingResponse)(nil), // 1: uber.submitqueue.stovepipe.PingResponse + (*IngestRequest)(nil), // 2: uber.submitqueue.stovepipe.IngestRequest + (*IngestResponse)(nil), // 3: uber.submitqueue.stovepipe.IngestResponse + (*ValidationResult)(nil), // 4: uber.submitqueue.stovepipe.ValidationResult + (*GetProjectStatusByURIRequest)(nil), // 5: uber.submitqueue.stovepipe.GetProjectStatusByURIRequest + (*ProjectValidation)(nil), // 6: uber.submitqueue.stovepipe.ProjectValidation + (*GetProjectStatusByURIResponse)(nil), // 7: uber.submitqueue.stovepipe.GetProjectStatusByURIResponse } var file_stovepipe_proto_depIdxs = []int32{ - 0, // 0: uber.submitqueue.stovepipe.Stovepipe.Ping:input_type -> uber.submitqueue.stovepipe.PingRequest - 2, // 1: uber.submitqueue.stovepipe.Stovepipe.Ingest:input_type -> uber.submitqueue.stovepipe.IngestRequest - 1, // 2: uber.submitqueue.stovepipe.Stovepipe.Ping:output_type -> uber.submitqueue.stovepipe.PingResponse - 3, // 3: uber.submitqueue.stovepipe.Stovepipe.Ingest:output_type -> uber.submitqueue.stovepipe.IngestResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 4, // 0: uber.submitqueue.stovepipe.ProjectValidation.result:type_name -> uber.submitqueue.stovepipe.ValidationResult + 4, // 1: uber.submitqueue.stovepipe.GetProjectStatusByURIResponse.repository_result:type_name -> uber.submitqueue.stovepipe.ValidationResult + 6, // 2: uber.submitqueue.stovepipe.GetProjectStatusByURIResponse.projects:type_name -> uber.submitqueue.stovepipe.ProjectValidation + 0, // 3: uber.submitqueue.stovepipe.Stovepipe.Ping:input_type -> uber.submitqueue.stovepipe.PingRequest + 2, // 4: uber.submitqueue.stovepipe.Stovepipe.Ingest:input_type -> uber.submitqueue.stovepipe.IngestRequest + 5, // 5: uber.submitqueue.stovepipe.Stovepipe.GetProjectStatusByURI:input_type -> uber.submitqueue.stovepipe.GetProjectStatusByURIRequest + 1, // 6: uber.submitqueue.stovepipe.Stovepipe.Ping:output_type -> uber.submitqueue.stovepipe.PingResponse + 3, // 7: uber.submitqueue.stovepipe.Stovepipe.Ingest:output_type -> uber.submitqueue.stovepipe.IngestResponse + 7, // 8: uber.submitqueue.stovepipe.Stovepipe.GetProjectStatusByURI:output_type -> uber.submitqueue.stovepipe.GetProjectStatusByURIResponse + 6, // [6:9] is the sub-list for method output_type + 3, // [3:6] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_stovepipe_proto_init() } @@ -307,13 +646,14 @@ func file_stovepipe_proto_init() { if File_stovepipe_proto != nil { return } + file_stovepipe_proto_msgTypes[5].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_stovepipe_proto_rawDesc), len(file_stovepipe_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 8, NumExtensions: 0, NumServices: 1, }, diff --git a/api/stovepipe/protopb/stovepipe.pb.yarpc.go b/api/stovepipe/protopb/stovepipe.pb.yarpc.go index ad6fe9eb5..da15704ae 100644 --- a/api/stovepipe/protopb/stovepipe.pb.yarpc.go +++ b/api/stovepipe/protopb/stovepipe.pb.yarpc.go @@ -23,6 +23,7 @@ var _ = ioutil.NopCloser type StovepipeYARPCClient interface { Ping(context.Context, *PingRequest, ...yarpc.CallOption) (*PingResponse, error) Ingest(context.Context, *IngestRequest, ...yarpc.CallOption) (*IngestResponse, error) + GetProjectStatusByURI(context.Context, *GetProjectStatusByURIRequest, ...yarpc.CallOption) (*GetProjectStatusByURIResponse, error) } func newStovepipeYARPCClient(clientConfig transport.ClientConfig, anyResolver v2.AnyResolver, options ...v2.ClientOption) StovepipeYARPCClient { @@ -45,6 +46,7 @@ func NewStovepipeYARPCClient(clientConfig transport.ClientConfig, options ...v2. type StovepipeYARPCServer interface { Ping(context.Context, *PingRequest) (*PingResponse, error) Ingest(context.Context, *IngestRequest) (*IngestResponse, error) + GetProjectStatusByURI(context.Context, *GetProjectStatusByURIRequest) (*GetProjectStatusByURIResponse, error) } type buildStovepipeYARPCProceduresParams struct { @@ -78,6 +80,16 @@ func buildStovepipeYARPCProcedures(params buildStovepipeYARPCProceduresParams) [ }, ), }, + { + MethodName: "GetProjectStatusByURI", + Handler: v2.NewUnaryHandler( + v2.UnaryHandlerParams{ + Handle: handler.GetProjectStatusByURI, + NewRequest: newStovepipeServiceGetProjectStatusByURIYARPCRequest, + AnyResolver: params.AnyResolver, + }, + ), + }, }, OnewayHandlerParams: []v2.BuildProceduresOnewayHandlerParams{}, StreamHandlerParams: []v2.BuildProceduresStreamHandlerParams{}, @@ -214,6 +226,18 @@ func (c *_StovepipeYARPCCaller) Ingest(ctx context.Context, request *IngestReque return response, err } +func (c *_StovepipeYARPCCaller) GetProjectStatusByURI(ctx context.Context, request *GetProjectStatusByURIRequest, options ...yarpc.CallOption) (*GetProjectStatusByURIResponse, error) { + responseMessage, err := c.streamClient.Call(ctx, "GetProjectStatusByURI", request, newStovepipeServiceGetProjectStatusByURIYARPCResponse, options...) + if responseMessage == nil { + return nil, err + } + response, ok := responseMessage.(*GetProjectStatusByURIResponse) + if !ok { + return nil, v2.CastError(emptyStovepipeServiceGetProjectStatusByURIYARPCResponse, responseMessage) + } + return response, err +} + type _StovepipeYARPCHandler struct { server StovepipeYARPCServer } @@ -250,6 +274,22 @@ func (h *_StovepipeYARPCHandler) Ingest(ctx context.Context, requestMessage prot return response, err } +func (h *_StovepipeYARPCHandler) GetProjectStatusByURI(ctx context.Context, requestMessage proto.Message) (proto.Message, error) { + var request *GetProjectStatusByURIRequest + var ok bool + if requestMessage != nil { + request, ok = requestMessage.(*GetProjectStatusByURIRequest) + if !ok { + return nil, v2.CastError(emptyStovepipeServiceGetProjectStatusByURIYARPCRequest, requestMessage) + } + } + response, err := h.server.GetProjectStatusByURI(ctx, request) + if response == nil { + return nil, err + } + return response, err +} + func newStovepipeServicePingYARPCRequest() proto.Message { return &PingRequest{} } @@ -266,36 +306,66 @@ func newStovepipeServiceIngestYARPCResponse() proto.Message { return &IngestResponse{} } +func newStovepipeServiceGetProjectStatusByURIYARPCRequest() proto.Message { + return &GetProjectStatusByURIRequest{} +} + +func newStovepipeServiceGetProjectStatusByURIYARPCResponse() proto.Message { + return &GetProjectStatusByURIResponse{} +} + var ( - emptyStovepipeServicePingYARPCRequest = &PingRequest{} - emptyStovepipeServicePingYARPCResponse = &PingResponse{} - emptyStovepipeServiceIngestYARPCRequest = &IngestRequest{} - emptyStovepipeServiceIngestYARPCResponse = &IngestResponse{} + emptyStovepipeServicePingYARPCRequest = &PingRequest{} + emptyStovepipeServicePingYARPCResponse = &PingResponse{} + emptyStovepipeServiceIngestYARPCRequest = &IngestRequest{} + emptyStovepipeServiceIngestYARPCResponse = &IngestResponse{} + emptyStovepipeServiceGetProjectStatusByURIYARPCRequest = &GetProjectStatusByURIRequest{} + emptyStovepipeServiceGetProjectStatusByURIYARPCResponse = &GetProjectStatusByURIResponse{} ) var yarpcFileDescriptorClosurefabdb6b3c0b09022 = [][]byte{ // stovepipe.proto []byte{ - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0x4f, 0x4e, 0xf3, 0x30, - 0x10, 0xc5, 0x3f, 0xb7, 0xfd, 0x0a, 0x9d, 0x96, 0x22, 0x59, 0x2c, 0xa2, 0x08, 0xa1, 0x12, 0x09, - 0xb5, 0xb0, 0x70, 0x04, 0xdc, 0xa0, 0x3b, 0x36, 0xa8, 0x0a, 0x3b, 0x58, 0x20, 0x27, 0x1d, 0xa5, - 0x5e, 0x38, 0x76, 0x63, 0xbb, 0x37, 0xe0, 0x74, 0x5c, 0x0a, 0xc5, 0xf9, 0x43, 0x59, 0xb4, 0xb0, - 0xf3, 0x8c, 0xdf, 0x4f, 0xf3, 0xde, 0x68, 0xe0, 0xdc, 0x58, 0xb5, 0x43, 0x2d, 0x34, 0x32, 0x5d, - 0x2a, 0xab, 0x68, 0xe8, 0x52, 0x2c, 0x99, 0x71, 0xa9, 0x14, 0x76, 0xeb, 0xd0, 0x21, 0xeb, 0x14, - 0xd1, 0x1c, 0xc6, 0x2b, 0x51, 0xe4, 0x09, 0x6e, 0x1d, 0x1a, 0x4b, 0x03, 0x38, 0x91, 0x68, 0x0c, - 0xcf, 0x31, 0x20, 0x33, 0xb2, 0x18, 0x25, 0x6d, 0x19, 0x7d, 0x10, 0x98, 0xd4, 0x4a, 0xa3, 0x55, - 0x61, 0xf0, 0xb0, 0x94, 0x5e, 0xc3, 0xc4, 0x60, 0xb9, 0x13, 0x19, 0xbe, 0x17, 0x5c, 0x62, 0xd0, - 0xf3, 0xdf, 0xe3, 0xa6, 0xf7, 0xcc, 0x25, 0xd2, 0x4b, 0x18, 0x59, 0x21, 0xd1, 0x58, 0x2e, 0x75, - 0xd0, 0x9f, 0x91, 0x45, 0x3f, 0xf9, 0x6e, 0xd0, 0x10, 0x4e, 0x37, 0xca, 0x58, 0x0f, 0x0f, 0x3c, - 0xdc, 0xd5, 0xd1, 0x0d, 0x9c, 0x3d, 0x15, 0x39, 0x1a, 0xdb, 0x5a, 0xbe, 0x80, 0xff, 0x3e, 0x54, - 0xe3, 0xa2, 0x2e, 0xa2, 0x19, 0x4c, 0x5b, 0x59, 0xe3, 0x77, 0x0a, 0x3d, 0xb1, 0x6e, 0x44, 0x3d, - 0xb1, 0x7e, 0xf8, 0x24, 0x30, 0x7a, 0x69, 0xf7, 0x40, 0xdf, 0x60, 0x50, 0xa5, 0xa3, 0x73, 0x76, - 0x78, 0x59, 0x6c, 0x6f, 0x53, 0xe1, 0xe2, 0x77, 0x61, 0x3d, 0x38, 0xfa, 0x47, 0x39, 0x0c, 0x6b, - 0x33, 0xf4, 0xf6, 0x18, 0xf5, 0x23, 0x57, 0x78, 0xf7, 0x17, 0x69, 0x3b, 0x62, 0x89, 0x70, 0x95, - 0x29, 0x79, 0x04, 0x59, 0x4e, 0xbb, 0xb0, 0xab, 0xea, 0x2a, 0x56, 0xe4, 0xf5, 0x3e, 0x17, 0x76, - 0xe3, 0x52, 0x96, 0x29, 0x19, 0x57, 0x60, 0xbc, 0x07, 0xc6, 0x5c, 0x8b, 0xb8, 0x83, 0x63, 0x7f, - 0x48, 0x3a, 0x4d, 0x87, 0xfe, 0xf1, 0xf8, 0x15, 0x00, 0x00, 0xff, 0xff, 0xc1, 0x6e, 0x43, 0xbc, - 0x64, 0x02, 0x00, 0x00, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x4d, 0x6f, 0xd3, 0x40, + 0x10, 0xad, 0x93, 0x36, 0xb1, 0x27, 0xfd, 0x5c, 0x41, 0x65, 0x42, 0x83, 0x82, 0x11, 0x34, 0x54, + 0x90, 0x88, 0x72, 0x29, 0xd7, 0x80, 0x04, 0xb9, 0xa0, 0xc8, 0xa5, 0x48, 0xc0, 0xc1, 0xda, 0x24, + 0x23, 0x77, 0xa1, 0xf6, 0xba, 0xde, 0x75, 0x45, 0x7b, 0x47, 0xe2, 0xc4, 0x0f, 0x82, 0xbf, 0xc2, + 0x8f, 0x41, 0xde, 0x5d, 0x3b, 0xa5, 0x10, 0x83, 0x7a, 0xf3, 0xbc, 0x99, 0xf7, 0x76, 0xfc, 0x66, + 0x76, 0x61, 0x43, 0x48, 0x7e, 0x86, 0x09, 0x4b, 0xb0, 0x9f, 0xa4, 0x5c, 0x72, 0xd2, 0xce, 0x26, + 0x98, 0xf6, 0x45, 0x36, 0x89, 0x98, 0x3c, 0xcd, 0x30, 0xc3, 0x7e, 0x59, 0xe1, 0xed, 0x42, 0x6b, + 0xcc, 0xe2, 0xd0, 0xc7, 0xd3, 0x0c, 0x85, 0x24, 0x2e, 0x34, 0x23, 0x14, 0x82, 0x86, 0xe8, 0x5a, + 0x5d, 0xab, 0xe7, 0xf8, 0x45, 0xe8, 0x7d, 0xb1, 0x60, 0x55, 0x57, 0x8a, 0x84, 0xc7, 0x02, 0x17, + 0x97, 0x92, 0xbb, 0xb0, 0x2a, 0x30, 0x3d, 0x63, 0x53, 0x0c, 0x62, 0x1a, 0xa1, 0x5b, 0x53, 0xe9, + 0x96, 0xc1, 0x5e, 0xd3, 0x08, 0xc9, 0x0e, 0x38, 0x92, 0x45, 0x28, 0x24, 0x8d, 0x12, 0xb7, 0xde, + 0xb5, 0x7a, 0x75, 0x7f, 0x0e, 0x90, 0x36, 0xd8, 0xc7, 0x5c, 0x48, 0x45, 0x5e, 0x56, 0xe4, 0x32, + 0xf6, 0xee, 0xc3, 0xda, 0x28, 0x0e, 0x51, 0xc8, 0xa2, 0xe5, 0x1b, 0xb0, 0xa2, 0x7e, 0xca, 0x74, + 0xa1, 0x03, 0xaf, 0x0b, 0xeb, 0x45, 0x99, 0xe9, 0x77, 0x1d, 0x6a, 0x6c, 0x66, 0x8a, 0x6a, 0x6c, + 0xe6, 0xed, 0xc1, 0xe6, 0x5b, 0x7a, 0xc2, 0x66, 0x54, 0x32, 0x1e, 0xfb, 0x28, 0xb2, 0x13, 0x49, + 0xb6, 0xa1, 0x31, 0xc3, 0x30, 0x45, 0x2d, 0x66, 0xf9, 0x26, 0xf2, 0xbe, 0x5b, 0xb0, 0xf3, 0x12, + 0xe5, 0x38, 0xe5, 0x1f, 0x71, 0x2a, 0x0f, 0x25, 0x95, 0x99, 0x18, 0x9e, 0x1f, 0xf9, 0xa3, 0xca, + 0x26, 0x48, 0x07, 0x60, 0x7a, 0x4c, 0xe3, 0x10, 0x83, 0x2c, 0x65, 0xc6, 0x06, 0x47, 0x23, 0x47, + 0x29, 0x23, 0x1d, 0x68, 0x26, 0x5a, 0x51, 0x59, 0xe0, 0xbc, 0x5a, 0xf2, 0x0b, 0xe0, 0xab, 0x65, + 0x91, 0xdb, 0xe0, 0x24, 0x34, 0xc4, 0x40, 0xb0, 0x0b, 0x6d, 0xc3, 0x8a, 0x6f, 0xe7, 0xc0, 0x21, + 0xbb, 0x50, 0xd2, 0x2a, 0x29, 0xf9, 0x27, 0x8c, 0xdd, 0x15, 0x2d, 0x9d, 0x23, 0x6f, 0x72, 0x60, + 0x08, 0x60, 0x07, 0x46, 0xca, 0x13, 0xb0, 0x65, 0x1a, 0x9f, 0xff, 0x6f, 0x3e, 0xbd, 0xe2, 0x6c, + 0x33, 0x3d, 0x13, 0x92, 0x17, 0xd0, 0x48, 0x95, 0x1b, 0xaa, 0xe1, 0xd6, 0xfe, 0xa3, 0xfe, 0xe2, + 0xf5, 0xe9, 0x5f, 0x75, 0xd0, 0x37, 0x5c, 0xef, 0x47, 0x1d, 0x3a, 0x0b, 0x1c, 0x33, 0xf3, 0xe8, + 0x00, 0xa4, 0xda, 0xbd, 0xa0, 0x9c, 0x8b, 0x63, 0x90, 0xd1, 0x6c, 0xee, 0x68, 0x6d, 0xb1, 0xa3, + 0xf5, 0xab, 0x8e, 0xde, 0x02, 0x7b, 0x42, 0x85, 0x4e, 0xea, 0xc5, 0x69, 0xe6, 0x71, 0x9e, 0xba, + 0x07, 0x6b, 0xc5, 0x71, 0x42, 0x52, 0x89, 0xc6, 0xb3, 0x55, 0x03, 0xe6, 0x1d, 0x22, 0x79, 0x07, + 0x5b, 0x29, 0x26, 0x5c, 0x30, 0xc9, 0xd3, 0xf3, 0xc0, 0xd8, 0xd0, 0xb8, 0x86, 0x0d, 0x9b, 0x73, + 0x19, 0xb3, 0x5a, 0x07, 0xe0, 0x1a, 0x87, 0x8d, 0xae, 0x08, 0xa6, 0x3c, 0x4a, 0x4e, 0x50, 0xa2, + 0xdb, 0xec, 0x5a, 0x3d, 0xdb, 0xdf, 0x36, 0x79, 0x4d, 0x10, 0xcf, 0x4d, 0x96, 0x8c, 0xc0, 0x36, + 0x19, 0xe1, 0xda, 0xdd, 0x7a, 0xaf, 0xb5, 0xff, 0xb8, 0xaa, 0x97, 0x3f, 0x66, 0xed, 0x97, 0x74, + 0xf2, 0x00, 0x36, 0x62, 0xfc, 0x2c, 0x83, 0x4b, 0xab, 0xe3, 0x28, 0x1b, 0xd6, 0x72, 0x78, 0x5c, + 0xac, 0xcf, 0xfe, 0xcf, 0x1a, 0x38, 0x87, 0x85, 0x22, 0xf9, 0x00, 0xcb, 0xf9, 0xcd, 0x27, 0xbb, + 0x95, 0xc7, 0xce, 0x5f, 0x91, 0x76, 0xef, 0xdf, 0x85, 0x7a, 0x09, 0xbc, 0x25, 0x42, 0xa1, 0xa1, + 0x2f, 0x2a, 0x79, 0x58, 0xc5, 0xfa, 0xed, 0xce, 0xb7, 0xf7, 0xfe, 0xa7, 0xb4, 0x3c, 0xe2, 0x9b, + 0x05, 0x37, 0xff, 0xba, 0x8b, 0xe4, 0xa0, 0x4a, 0xa7, 0xea, 0xc2, 0xb7, 0x9f, 0x5d, 0x83, 0x59, + 0x34, 0x34, 0x44, 0xb8, 0x33, 0xe5, 0x51, 0x85, 0xc2, 0x70, 0xbd, 0x74, 0x7f, 0x9c, 0x3f, 0xe1, + 0x63, 0xeb, 0xfd, 0x93, 0x90, 0xc9, 0xe3, 0x6c, 0xd2, 0x9f, 0xf2, 0x68, 0x90, 0x13, 0x07, 0x97, + 0x88, 0x03, 0x9a, 0xb0, 0x41, 0x49, 0x1e, 0xa8, 0x57, 0x3f, 0x99, 0x4c, 0x1a, 0xea, 0xe3, 0xe9, + 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbf, 0x12, 0x29, 0x6d, 0x11, 0x06, 0x00, 0x00, }, } diff --git a/api/stovepipe/protopb/stovepipe_grpc.pb.go b/api/stovepipe/protopb/stovepipe_grpc.pb.go index ec4f58a47..4adad2ff9 100644 --- a/api/stovepipe/protopb/stovepipe_grpc.pb.go +++ b/api/stovepipe/protopb/stovepipe_grpc.pb.go @@ -34,8 +34,9 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Stovepipe_Ping_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/Ping" - Stovepipe_Ingest_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/Ingest" + Stovepipe_Ping_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/Ping" + Stovepipe_Ingest_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/Ingest" + Stovepipe_GetProjectStatusByURI_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/GetProjectStatusByURI" ) // StovepipeClient is the client API for Stovepipe service. @@ -49,6 +50,8 @@ type StovepipeClient interface { // Ingest admits a queue's newly observed commit into the validation pipeline and returns // the minted request ID. The caller hands off asynchronously; validation happens later. Ingest(ctx context.Context, in *IngestRequest, opts ...grpc.CallOption) (*IngestResponse, error) + // GetProjectStatusByURI returns current validation for an exact commit URI's authoritative request. + GetProjectStatusByURI(ctx context.Context, in *GetProjectStatusByURIRequest, opts ...grpc.CallOption) (*GetProjectStatusByURIResponse, error) } type stovepipeClient struct { @@ -79,6 +82,16 @@ func (c *stovepipeClient) Ingest(ctx context.Context, in *IngestRequest, opts .. return out, nil } +func (c *stovepipeClient) GetProjectStatusByURI(ctx context.Context, in *GetProjectStatusByURIRequest, opts ...grpc.CallOption) (*GetProjectStatusByURIResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetProjectStatusByURIResponse) + err := c.cc.Invoke(ctx, Stovepipe_GetProjectStatusByURI_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // StovepipeServer is the server API for Stovepipe service. // All implementations must embed UnimplementedStovepipeServer // for forward compatibility. @@ -90,6 +103,8 @@ type StovepipeServer interface { // Ingest admits a queue's newly observed commit into the validation pipeline and returns // the minted request ID. The caller hands off asynchronously; validation happens later. Ingest(context.Context, *IngestRequest) (*IngestResponse, error) + // GetProjectStatusByURI returns current validation for an exact commit URI's authoritative request. + GetProjectStatusByURI(context.Context, *GetProjectStatusByURIRequest) (*GetProjectStatusByURIResponse, error) mustEmbedUnimplementedStovepipeServer() } @@ -106,6 +121,9 @@ func (UnimplementedStovepipeServer) Ping(context.Context, *PingRequest) (*PingRe func (UnimplementedStovepipeServer) Ingest(context.Context, *IngestRequest) (*IngestResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Ingest not implemented") } +func (UnimplementedStovepipeServer) GetProjectStatusByURI(context.Context, *GetProjectStatusByURIRequest) (*GetProjectStatusByURIResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetProjectStatusByURI not implemented") +} func (UnimplementedStovepipeServer) mustEmbedUnimplementedStovepipeServer() {} func (UnimplementedStovepipeServer) testEmbeddedByValue() {} @@ -163,6 +181,24 @@ func _Stovepipe_Ingest_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } +func _Stovepipe_GetProjectStatusByURI_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProjectStatusByURIRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StovepipeServer).GetProjectStatusByURI(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Stovepipe_GetProjectStatusByURI_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StovepipeServer).GetProjectStatusByURI(ctx, req.(*GetProjectStatusByURIRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Stovepipe_ServiceDesc is the grpc.ServiceDesc for Stovepipe service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -178,6 +214,10 @@ var Stovepipe_ServiceDesc = grpc.ServiceDesc{ MethodName: "Ingest", Handler: _Stovepipe_Ingest_Handler, }, + { + MethodName: "GetProjectStatusByURI", + Handler: _Stovepipe_GetProjectStatusByURI_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "stovepipe.proto", diff --git a/doc/rfc/index.md b/doc/rfc/index.md index aa4f41311..f99d93ccf 100644 --- a/doc/rfc/index.md +++ b/doc/rfc/index.md @@ -29,6 +29,7 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting - [Build stage](stovepipe/steps/build.md) - Trigger-only stage and Stovepipe's URI-based BuildRunner contract - [Buildsignal stage](stovepipe/steps/buildsignal.md) - Build polling, terminal status persistence, and the handoff to record - [Record stage](stovepipe/steps/record.md) - Immutable validation facts keyed by `(queue, uri, project)`, monotonic last-green bookmark advancement and ref promotion, and the deferred hook-event and analyze handoffs +- [GetProjectStatusByURI API](stovepipe/get-project-status-by-uri-api.md) - Current request lifecycle, whole-repository result, and cursor-paginated project validation results backed by one durable project manifest ## Runway diff --git a/doc/rfc/stovepipe/get-project-status-by-uri-api.md b/doc/rfc/stovepipe/get-project-status-by-uri-api.md new file mode 100644 index 000000000..b6be2ca86 --- /dev/null +++ b/doc/rfc/stovepipe/get-project-status-by-uri-api.md @@ -0,0 +1,112 @@ +# Stovepipe GetProjectStatusByURI API + +## Summary + +Stovepipe exposes the current validation of one queue and commit through `GetProjectStatusByURI`. The commit URI resolves to its newest authoritative request. The response combines that request's lifecycle, whole-repository result, and complete planned project list with any results recorded for those projects. + +This is a current-state API, not another history projection. It reads `Request` for lifecycle and scope, `ValidationFact` for immutable results, and a request-owned project manifest for enumeration and completion. The manifest is also the single project list used by validation planning and downstream consumers. + +The contract is based on the validation lookup flow in [Stovepipe <-> CD Integration: Event and API Contract](https://docs.google.com/document/d/1ouymU93l2a6lLiKwhqViSR1HFdwviuywj_sL6gDNeQM/edit?tab=t.n2tdz7ihz0sk). + +## Representative Contract + +The final protobuf receives a separate compatibility review before implementation. Its representative shape is: + +```proto +message GetProjectStatusByURIRequest { + string queue = 1; + string change_uri = 2; + optional string project = 3; + int32 page_size = 4; + string page_token = 5; +} + +message ValidationResult { + double degree = 1; +} + +message ProjectValidation { + string project = 1; + ValidationResult result = 2; +} + +message GetProjectStatusByURIResponse { + string request_id = 1; + string queue = 2; + string change_uri = 3; + string base_uri = 4; + string request_state = 5; + ValidationResult repository_result = 6; + bool project_results_complete = 7; + repeated ProjectValidation projects = 8; + string next_page_token = 9; +} + +service Stovepipe { + rpc GetProjectStatusByURI(GetProjectStatusByURIRequest) returns (GetProjectStatusByURIResponse) {} +} +``` + +Message fields preserve presence. An absent `repository_result` or `ProjectValidation.result` means no fact has been recorded; it never means green. `project_results_complete` is true only after every project in the persisted manifest has exactly one durable result and the request's completion marker has been written. + +The existing degree scale answers “how broken is this scope”: `0.0` is fully green, `1.0` is fully broken, and project analysis may assign intermediate values. The API preserves those Stovepipe semantics rather than inverting the scale at the wire boundary. + +## Selection and Projection + +`queue` and `change_uri` resolve one request through the durable authoritative request-URI mapping. The current insert-once mapping selects the only request; future revalidation advances it to the newest accepted request without changing the API. The controller binds storage to the queue, reads `request_uri` by its `(queue, URI)` primary key, then reads `request` by its `(queue, request ID)` primary key. It does not scan or query the Request table by an attribute. Once a newer request becomes authoritative, its failure does not fall back to an older successful attempt. + +The controller verifies that the loaded Request has the requested queue and URI before loading its project manifest and whole-repository fact. Request URI is immutable once the request and mapping are created; storage updates must not permit the two records to diverge. + +Ingest claims `request_uri` before creating the Request so concurrent ingests converge without a cross-record transaction. A lookup racing that sequence can therefore observe a mapping whose Request is not present yet. `GetProjectStatusByURI` treats that condition as unavailable and retryable rather than not found or immediate corruption. Repeated occurrences are surfaced through consistency metrics and require repair; a mapping to a Request with a different queue or URI is an internal consistency error. + +`request_state` uses the stable public vocabulary `accepted`, `processing`, `succeeded`, `failed`, `cancelled`, and `superseded`. One value identifies both lifecycle position and terminal outcome without permitting contradictory field combinations. The wire field is a string, following SubmitQueue's current-status and history APIs, so clients can tolerate additive vocabulary changes. It is projected explicitly from Stovepipe's internal `RequestState`; no SubmitQueue domain enum is shared across the boundary. + +`failed` retains the current conservative meaning: either validation failed or the request could not continue. The API does not synthesize a `COMPLETED` versus `ERROR` distinction that the Request does not persist. Adding that distinction later requires a durable outcome classification first, followed by an additive response field. + +When `project` is omitted, the response contains one bounded page of the manifest, including planned projects whose result is not yet present. When `project` is present, it must be a non-empty project in the manifest; the response contains only that project and no continuation token. A nonzero page size or nonempty page token with an exact project selector is invalid. + +The whole-repository fact is independent of project facts. The controller does not infer project results from it. An initial coarse implementation may record the same derived outcome for every planned project, but it still persists the project list and each project fact so clients receive the same contract when finer analysis arrives. + +## Project Manifest and Completion + +Project analysis creates one immutable, request-owned logical manifest containing unique project IDs in ascending bytewise order. It persists the manifest before recording project facts. The manifest is addressed by request ID and serves as the index for exact `ValidationFactStore.Get(changeURI, project)` reads; `ValidationFactStore` does not gain `ListByURI` or another secondary-index query. + +The storage representation does not require the entire list to fit in one row. A backend may keep a root record keyed by request ID that names immutable deterministic chunks and their ordered boundaries. This preserves exact get-by-key operations while allowing page reads to avoid loading a service list that was already too large for an event payload. + +The project list used to execute validation is the list returned by this API. No separate response-only service list is maintained. A retry that proposes a different list for the same request is an invariant violation rather than an in-place replacement. + +After facts have been recorded, the owner of project analysis verifies one fact for every manifest entry using exact reads and writes a durable, idempotent completion marker. Fact creation remains independent and retryable; no multi-record transaction or count query is required. Empty manifests may complete immediately. + +Request terminal state and result completion are intentionally distinct. The current pipeline makes the request terminal before `record` persists its fact, so terminal state alone cannot promise result availability. A downstream `end_validation` event is published only after the whole-repository result is durable, the completion marker is durable, and every planned project has exactly one durable result. Persist-before-publish makes a failed publish safe to retry. + +## Pagination and Consistency + +Pagination follows SubmitQueue's queue-listing convention: an empty token selects the first page, zero page size selects the server default, and `next_page_token` is empty on the final page. The initial default is 50 projects and the maximum is 200. + +The controller reads the immutable manifest in `project ASC` order and inspects one project beyond the effective page size before issuing a continuation token. The opaque, versioned token represents the exclusive position after the last returned project and is bound to the queue, change URI, resolved request ID, and all-project selector mode. Binding the resolved request ID keeps an in-progress traversal on the same validation if a newer request becomes authoritative between pages. Decoding produces a typed manifest cursor; neither the manifest store nor the fact store parses public tokens. The cursor may include a deterministic chunk position in addition to the last project ID without exposing either representation to clients. Page size is not bound, so callers may change it between pages. A malformed token, unsupported version, or token reused for another query is invalid. + +After reading one manifest page, the controller loads its project facts through bounded parallel exact reads. The caller owns that concurrency; the fact-store contract remains a portable single-key `Get` rather than requiring SQL `IN`, batch atomicity, or a secondary index. + +The manifest is immutable, so pages neither skip nor duplicate project identities. Facts are create-only and may appear between reads: an incomplete traversal can observe more results on later pages, while a new traversal reflects all facts available at its start. `project_results_complete` is the durable signal that no planned result remains absent; it is not computed by counting the current page. + +## Storage Identity and Evolution + +URI, request ID, queue, and project identity use byte-exact comparison. MySQL schemas declare an explicit binary collation for these key columns rather than inherit the server's case- or accent-insensitive default. API validation and storage schemas use the same explicit length limits, so an oversized selector is rejected before lookup rather than failing or truncating inside a backend. + +The insert-once request-URI mapping selects the only request today. Supporting revalidation of the same URI widens validation-fact identity and changes the mapping into an explicitly versioned authoritative-request pointer advanced only after the new Request is durable. The point lookup remains singular; discovering older attempts can be added independently as a request-list API. + +## Errors and Authorization + +- Empty queue or change URI is invalid. +- An empty explicit project, invalid page size, page fields used with a project selector, or invalid token is invalid. +- An unknown queue-scoped change URI or a project absent from its manifest is not found. +- A URI mapping whose Request is not yet visible is unavailable and retryable; a mismatched Request, missing manifest for a request that should have been planned, a completion marker with any missing fact, or a fact attributed to another request is an internal consistency error. +- Retryable storage failures are unavailable; context cancellation and deadline errors retain their canonical codes. + +Authorization follows the queue policy used by other Stovepipe reads. Both the request-URI mapping and every loaded entity remain scoped to the supplied queue. + +## Rollout and Testing + +The endpoint is enabled only after project-manifest persistence, project-fact recording, the completion marker, and `end_validation` publish ordering are deployed. This avoids exposing a nominal all-project response that silently omits the service list. + +Contract and controller tests cover request-state projection, result presence at degree zero, whole-repository and project independence, exact project selection, incomplete and complete manifests, empty and chunked manifests, first/middle/final pages, token query binding, stable ordering, cross-queue isolation, mapping-before-Request retries, mismatched identities, byte-distinct keys, storage limits, and publish-after-completion ordering.