From b3d1fab59405b7b85b3578aa973b5cef37faac2c Mon Sep 17 00:00:00 2001 From: mnoah1 Date: Thu, 27 Aug 2026 17:24:54 +0000 Subject: [PATCH] feat(stovepipe): define GetProjectStatusByURI API --- api/stovepipe/proto/stovepipe.proto | 54 +++ api/stovepipe/protopb/stovepipe.pb.go | 334 +++++++++++++++++- api/stovepipe/protopb/stovepipe.pb.yarpc.go | 119 +++++-- api/stovepipe/protopb/stovepipe_grpc.pb.go | 44 ++- doc/rfc/index.md | 1 + .../get-project-status-by-uri-api.md | 108 ++++++ 6 files changed, 617 insertions(+), 43 deletions(-) create mode 100644 doc/rfc/stovepipe/get-project-status-by-uri-api.md diff --git a/api/stovepipe/proto/stovepipe.proto b/api/stovepipe/proto/stovepipe.proto index a00decc9c..22ec77ebe 100644 --- a/api/stovepipe/proto/stovepipe.proto +++ b/api/stovepipe/proto/stovepipe.proto @@ -54,6 +54,58 @@ message IngestResponse { string id = 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 set, page_size and page_token must be zero and empty; + // when absent, one page of planned projects is returned. + optional string project = 3; + // Maximum projects to return. Zero selects the server default; negative values and values + // above the server maximum are invalid. + int32 page_size = 4; + // Opaque continuation token. Empty selects the first page. + string page_token = 5; +} + +// ProjectValidation contains one planned project and any recorded result. +message ProjectValidation { + // Stable project identifier. + string project = 1; + // Breakage degree measures how broken the project is on [0.0, 1.0]. Unset until its fact is + // recorded; zero is fully green and one is fully broken. Values are directly comparable: + // lower values are greener, and any value above zero is not green. + optional double breakage_degree = 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 and terminal outcome of the request. + string request_state = 5; + // Breakage degree measures how broken the whole repository is on [0.0, 1.0]. Unset until its + // fact is recorded; zero is fully green and one is fully broken. Values are directly + // comparable: lower values are greener, and any value above zero is not green. + optional double repository_breakage_degree = 6; + // Whether every planned project has one durable result and completion is recorded. False + // until the project list is available. + bool project_results_complete = 7; + // Planned projects in stable project order, with any recorded breakage degrees. Empty until + // the project list is available. + 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 +113,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..c490d7aa1 100644 --- a/api/stovepipe/protopb/stovepipe.pb.go +++ b/api/stovepipe/protopb/stovepipe.pb.go @@ -250,6 +250,269 @@ func (x *IngestResponse) GetId() string { return "" } +// 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 set, page_size and page_token must be zero and empty; + // 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; negative values and values + // above the server maximum are invalid. + 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[4] + 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[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 GetProjectStatusByURIRequest.ProtoReflect.Descriptor instead. +func (*GetProjectStatusByURIRequest) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{4} +} + +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 contains one planned project and any recorded result. +type ProjectValidation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable project identifier. + Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + // Breakage degree measures how broken the project is on [0.0, 1.0]. Unset until its fact is + // recorded; zero is fully green and one is fully broken. Values are directly comparable: + // lower values are greener, and any value above zero is not green. + BreakageDegree *float64 `protobuf:"fixed64,2,opt,name=breakage_degree,json=breakageDegree,proto3,oneof" json:"breakage_degree,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProjectValidation) Reset() { + *x = ProjectValidation{} + mi := &file_stovepipe_proto_msgTypes[5] + 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[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 ProjectValidation.ProtoReflect.Descriptor instead. +func (*ProjectValidation) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{5} +} + +func (x *ProjectValidation) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +func (x *ProjectValidation) GetBreakageDegree() float64 { + if x != nil && x.BreakageDegree != nil { + return *x.BreakageDegree + } + return 0 +} + +// 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 and terminal outcome of the request. + RequestState string `protobuf:"bytes,5,opt,name=request_state,json=requestState,proto3" json:"request_state,omitempty"` + // Breakage degree measures how broken the whole repository is on [0.0, 1.0]. Unset until its + // fact is recorded; zero is fully green and one is fully broken. Values are directly + // comparable: lower values are greener, and any value above zero is not green. + RepositoryBreakageDegree *float64 `protobuf:"fixed64,6,opt,name=repository_breakage_degree,json=repositoryBreakageDegree,proto3,oneof" json:"repository_breakage_degree,omitempty"` + // Whether every planned project has one durable result and completion is recorded. False + // until the project list is available. + 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 breakage degrees. Empty until + // the project list is available. + 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[6] + 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[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 GetProjectStatusByURIResponse.ProtoReflect.Descriptor instead. +func (*GetProjectStatusByURIResponse) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{6} +} + +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) GetRepositoryBreakageDegree() float64 { + if x != nil && x.RepositoryBreakageDegree != nil { + return *x.RepositoryBreakageDegree + } + return 0 +} + +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 +528,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\"\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\"o\n" + + "\x11ProjectValidation\x12\x18\n" + + "\aproject\x18\x01 \x01(\tR\aproject\x12,\n" + + "\x0fbreakage_degree\x18\x02 \x01(\x01H\x00R\x0ebreakageDegree\x88\x01\x01B\x12\n" + + "\x10_breakage_degree\"\xc2\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\x12A\n" + + "\x1arepository_breakage_degree\x18\x06 \x01(\x01H\x00R\x18repositoryBreakageDegree\x88\x01\x01\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\rnextPageTokenB\x1d\n" + + "\x1b_repository_breakage_degree2\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 +574,29 @@ 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, 7) 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 + (*GetProjectStatusByURIRequest)(nil), // 4: uber.submitqueue.stovepipe.GetProjectStatusByURIRequest + (*ProjectValidation)(nil), // 5: uber.submitqueue.stovepipe.ProjectValidation + (*GetProjectStatusByURIResponse)(nil), // 6: 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 + 5, // 0: uber.submitqueue.stovepipe.GetProjectStatusByURIResponse.projects:type_name -> uber.submitqueue.stovepipe.ProjectValidation + 0, // 1: uber.submitqueue.stovepipe.Stovepipe.Ping:input_type -> uber.submitqueue.stovepipe.PingRequest + 2, // 2: uber.submitqueue.stovepipe.Stovepipe.Ingest:input_type -> uber.submitqueue.stovepipe.IngestRequest + 4, // 3: uber.submitqueue.stovepipe.Stovepipe.GetProjectStatusByURI:input_type -> uber.submitqueue.stovepipe.GetProjectStatusByURIRequest + 1, // 4: uber.submitqueue.stovepipe.Stovepipe.Ping:output_type -> uber.submitqueue.stovepipe.PingResponse + 3, // 5: uber.submitqueue.stovepipe.Stovepipe.Ingest:output_type -> uber.submitqueue.stovepipe.IngestResponse + 6, // 6: uber.submitqueue.stovepipe.Stovepipe.GetProjectStatusByURI:output_type -> uber.submitqueue.stovepipe.GetProjectStatusByURIResponse + 4, // [4:7] is the sub-list for method output_type + 1, // [1:4] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_stovepipe_proto_init() } @@ -307,13 +604,16 @@ func file_stovepipe_proto_init() { if File_stovepipe_proto != nil { return } + file_stovepipe_proto_msgTypes[4].OneofWrappers = []any{} + file_stovepipe_proto_msgTypes[5].OneofWrappers = []any{} + file_stovepipe_proto_msgTypes[6].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: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/api/stovepipe/protopb/stovepipe.pb.yarpc.go b/api/stovepipe/protopb/stovepipe.pb.yarpc.go index ad6fe9eb5..7d10e9990 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,67 @@ 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, 0x41, 0x6f, 0xd3, 0x4c, + 0x10, 0xed, 0x26, 0x6d, 0x63, 0x4f, 0xdb, 0xf4, 0xfb, 0x56, 0x80, 0x4c, 0xda, 0xa0, 0x60, 0x04, + 0x0d, 0x08, 0x12, 0x51, 0x2e, 0xe5, 0x48, 0x40, 0xa2, 0xb9, 0xa0, 0xc8, 0xa5, 0x1c, 0xe0, 0x60, + 0xad, 0x93, 0x91, 0xbb, 0x34, 0xf6, 0xba, 0xde, 0x75, 0x45, 0x7b, 0x47, 0xea, 0x89, 0x1f, 0xc4, + 0x91, 0xdf, 0xc1, 0x8f, 0x41, 0xf6, 0xae, 0x9d, 0x12, 0x48, 0x40, 0xdc, 0xbc, 0xef, 0xbd, 0x99, + 0x9d, 0x79, 0x3b, 0x63, 0xd8, 0x96, 0x4a, 0x9c, 0x63, 0xc2, 0x13, 0xec, 0x25, 0xa9, 0x50, 0x82, + 0xb6, 0xb2, 0x00, 0xd3, 0x9e, 0xcc, 0x82, 0x88, 0xab, 0xb3, 0x0c, 0x33, 0xec, 0x55, 0x0a, 0x77, + 0x0f, 0x36, 0x46, 0x3c, 0x0e, 0x3d, 0x3c, 0xcb, 0x50, 0x2a, 0xea, 0x40, 0x23, 0x42, 0x29, 0x59, + 0x88, 0x0e, 0xe9, 0x90, 0xae, 0xed, 0x95, 0x47, 0xf7, 0x33, 0x81, 0x4d, 0xad, 0x94, 0x89, 0x88, + 0x25, 0x2e, 0x96, 0xd2, 0xbb, 0xb0, 0x29, 0x31, 0x3d, 0xe7, 0x63, 0xf4, 0x63, 0x16, 0xa1, 0x53, + 0x2b, 0xe8, 0x0d, 0x83, 0xbd, 0x61, 0x11, 0xd2, 0x5d, 0xb0, 0x15, 0x8f, 0x50, 0x2a, 0x16, 0x25, + 0x4e, 0xbd, 0x43, 0xba, 0x75, 0x6f, 0x06, 0xd0, 0x16, 0x58, 0x27, 0x42, 0xaa, 0x22, 0x78, 0xb5, + 0x08, 0xae, 0xce, 0xee, 0x7d, 0xd8, 0x1a, 0xc6, 0x21, 0x4a, 0x55, 0x96, 0x7c, 0x03, 0xd6, 0x8a, + 0xa6, 0x4c, 0x15, 0xfa, 0xe0, 0x76, 0xa0, 0x59, 0xca, 0x4c, 0xbd, 0x4d, 0xa8, 0xf1, 0x89, 0x11, + 0xd5, 0xf8, 0xc4, 0xfd, 0x4a, 0x60, 0xf7, 0x35, 0xaa, 0x51, 0x2a, 0x3e, 0xe2, 0x58, 0x1d, 0x29, + 0xa6, 0x32, 0x39, 0xb8, 0x38, 0xf6, 0x86, 0x4b, 0x13, 0xd3, 0x36, 0xc0, 0xf8, 0x84, 0xc5, 0x21, + 0xfa, 0x59, 0xca, 0x4d, 0x6b, 0xb6, 0x46, 0x8e, 0x53, 0x4e, 0xdb, 0xd0, 0x48, 0x74, 0xc6, 0xa2, + 0x2d, 0xfb, 0x70, 0xc5, 0x2b, 0x81, 0x2b, 0x42, 0xe8, 0x0e, 0xd8, 0x09, 0x0b, 0xd1, 0x97, 0xfc, + 0x52, 0xb7, 0xb6, 0xe6, 0x59, 0x39, 0x70, 0xc4, 0x2f, 0x8b, 0xd4, 0x05, 0xa9, 0xc4, 0x29, 0xc6, + 0xce, 0x9a, 0x4e, 0x9d, 0x23, 0x6f, 0x73, 0x60, 0x00, 0x60, 0xf9, 0x26, 0x95, 0x2b, 0xe0, 0x7f, + 0x53, 0xf8, 0x3b, 0x36, 0xe5, 0x13, 0xa6, 0xb8, 0x88, 0xf3, 0x17, 0x29, 0xef, 0x36, 0x2f, 0x62, + 0x8e, 0xf4, 0x31, 0x6c, 0x07, 0x29, 0xb2, 0xd3, 0x3c, 0xfb, 0x04, 0xc3, 0x14, 0xf5, 0xa3, 0x90, + 0xc3, 0x15, 0xaf, 0x59, 0x12, 0xaf, 0x0a, 0xfc, 0x8a, 0x90, 0x01, 0x85, 0xff, 0xfc, 0x39, 0xb9, + 0xfb, 0xad, 0x0e, 0xed, 0x05, 0x6e, 0x19, 0x7f, 0xdb, 0x00, 0xa9, 0x76, 0xce, 0xaf, 0x7c, 0xb6, + 0x0d, 0x32, 0x9c, 0xcc, 0xdc, 0xac, 0x2d, 0x76, 0xb3, 0x3e, 0xef, 0xe6, 0x6d, 0xb0, 0x02, 0x26, + 0x35, 0xa9, 0x07, 0xa1, 0x91, 0x9f, 0x73, 0xea, 0x1e, 0x6c, 0x95, 0xd7, 0x49, 0xc5, 0x14, 0x1a, + 0xbf, 0x36, 0x0d, 0x98, 0x57, 0x88, 0xf4, 0x05, 0xb4, 0x52, 0x4c, 0x84, 0xe4, 0x4a, 0xa4, 0x17, + 0xf3, 0x3d, 0x39, 0xeb, 0xc6, 0x02, 0x67, 0xa6, 0x19, 0xcc, 0x9b, 0x41, 0x0f, 0xc0, 0x31, 0x2e, + 0xfa, 0x29, 0xca, 0x6c, 0xaa, 0xa4, 0x3f, 0x16, 0x51, 0x32, 0x45, 0x85, 0x4e, 0xa3, 0x43, 0xba, + 0x96, 0x77, 0xcb, 0xf0, 0x9e, 0xa6, 0x5f, 0x1a, 0x96, 0x0e, 0xc1, 0x32, 0x8c, 0x74, 0xac, 0x4e, + 0xbd, 0xbb, 0xb1, 0xff, 0xa4, 0xb7, 0x78, 0x13, 0x7b, 0xbf, 0xbc, 0xa7, 0x57, 0x85, 0xd3, 0x07, + 0xb0, 0x1d, 0xe3, 0x27, 0xe5, 0x5f, 0x1b, 0x0f, 0xbb, 0x68, 0x77, 0x2b, 0x87, 0x47, 0xd5, 0x88, + 0xb4, 0x61, 0xc7, 0x5f, 0xdc, 0xf0, 0xfe, 0xf7, 0x1a, 0xd8, 0x47, 0xe5, 0x85, 0xf4, 0x03, 0xac, + 0xe6, 0x0b, 0x4d, 0xf7, 0x96, 0x56, 0x35, 0xfb, 0x39, 0xb4, 0xba, 0x7f, 0x16, 0xea, 0x59, 0x70, + 0x57, 0x28, 0x83, 0x75, 0xbd, 0x7f, 0xf4, 0xe1, 0xb2, 0xa8, 0x9f, 0x56, 0xb9, 0xf5, 0xe8, 0x6f, + 0xa4, 0xd5, 0x15, 0x5f, 0x08, 0xdc, 0xfc, 0xed, 0x48, 0xd2, 0x83, 0x65, 0x79, 0x96, 0xed, 0x7c, + 0xeb, 0xf9, 0x3f, 0x44, 0x96, 0x05, 0x0d, 0x10, 0xee, 0x8c, 0x45, 0xb4, 0x24, 0xc3, 0xa0, 0x59, + 0xb9, 0x3f, 0xca, 0xff, 0xcc, 0x23, 0xf2, 0xfe, 0x69, 0xc8, 0xd5, 0x49, 0x16, 0xf4, 0xc6, 0x22, + 0xea, 0xe7, 0x81, 0xfd, 0x6b, 0x81, 0x7d, 0x96, 0xf0, 0x7e, 0x15, 0xdc, 0x2f, 0x7e, 0xe6, 0x49, + 0x10, 0xac, 0x17, 0x1f, 0xcf, 0x7e, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb2, 0xa2, 0x62, 0x7b, 0xe8, + 0x05, 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..fbef626e3 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 paginated project results ## 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..59dff5f79 --- /dev/null +++ b/doc/rfc/stovepipe/get-project-status-by-uri-api.md @@ -0,0 +1,108 @@ +# Stovepipe GetProjectStatusByURI API + +## Summary + +Stovepipe exposes the current validation of a commit through `GetProjectStatusByURI`. The commit URI resolves to its newest authoritative request. Across one or more pages, the response combines that request's lifecycle, whole-repository result, and planned projects with any results recorded for them. + +The endpoint reads `Request` for lifecycle and scope, `ValidationFact` for immutable results, and a request-owned project list for enumerating individual project results. The project list will be populated once individual project mapping is implemented. + +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). + +## Published Contract + +The wire contract lives in [`api/stovepipe/proto/stovepipe.proto`](../../../api/stovepipe/proto/stovepipe.proto). `GetProjectStatusByURIRequest` selects a queue and exact change URI, with either an optional exact project or cursor pagination over all planned projects. + +The `repository_breakage_degree` and `ProjectValidation.breakage_degree` fields can be used to gate a commit or individual project. A missing degree means no result has been recorded; degree zero means fully green. + +## How the Response Is Built + +### Select the request + +1. Use `queue` to select the queue-bound storage implementation. + +2. Resolve `change_uri` to the authoritative `Request`. + +3. Verify that the loaded request has the requested queue and URI before loading any validation results. + +### Handle partially written state + +Ingest writes the request-URI mapping before it creates the `Request` so concurrent ingests converge without a cross-record transaction. A lookup may race between those writes. + +| State observed | API result | +| --- | --- | +| No request-URI mapping | Not found | +| Mapping exists but its `Request` is not visible yet | Unavailable and retryable | +| Mapping points to a `Request` with a different queue or URI | Internal consistency error | + +### Assemble the response + +| Response data | Durable source | Projection rule | +| --- | --- | --- | +| `request_id`, `queue`, `change_uri`, `base_uri` | `Request` | Copy from the verified authoritative request. | +| `request_state` | `Request.State` | Map to the stable public state vocabulary below. | +| `repository_breakage_degree` | Whole-repository `ValidationFact` | Leave absent until the fact exists; do not infer it from project results. | +| `projects` | Project list plus one exact `ValidationFact` read per project | Return every selected project even when its degree is not recorded yet. | +| `project_results_complete` | Durable completion marker | False until the project list is available; then set only after every listed project has a durable result. | +| `next_page_token` | Project list cursor | Return only when another project page exists. | + +`request_state` uses one value for both lifecycle position and terminal outcome: + +| Value | Meaning | +| --- | --- | +| `accepted` | The request is admitted but validation has not started. | +| `processing` | Validation is in progress. | +| `succeeded` | Validation reached a successful terminal outcome. | +| `failed` | Validation failed or the request could not continue. | +| `cancelled` | Validation was cancelled before reaching a verdict. | +| `superseded` | A newer head replaced the request before it ran. | + +The field is a string so clients can tolerate additive vocabulary changes. It is explicitly projected from Stovepipe's internal `RequestState`; no SubmitQueue domain enum crosses the boundary. The API does not invent a `COMPLETED` versus `ERROR` distinction that the request does not persist. Adding that distinction later requires a durable outcome classification and an additive response field. + +### Select projects + +| Request shape | Result | +| --- | --- | +| `project` omitted | Return one bounded page of the project list, including projects whose degree is not recorded yet. | +| Non-empty `project` present | Return that project only, with no continuation token. The project must exist in the project list. | +| Empty `project`, or page fields combined with an exact project | Reject as invalid. | + +The whole-repository fact and project facts are independent. The controller never infers one from the other. 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 List and Completion + +Each request owns one immutable project list. It contains unique project IDs in ascending bytewise order and is stored before any project results. The same list drives validation and the API; there is no separate response-only list. + +`project_results_complete` becomes true only after every project in the list has a durable result and a completion marker has been recorded. A persisted empty project list may complete immediately. A terminal `request_state` does not imply that results are complete. + +The `end_validation` event is published only after the whole-repository result and project completion are durable. If publishing fails, the durable state remains available and the event can be retried. + +## Pagination and Consistency + +| Rule | Behavior | +| --- | --- | +| Ordering | Projects are returned in ascending bytewise order. | +| `page_size` | Zero uses the default of 50; the maximum is 200. | +| `page_token` | Empty selects the first page. | +| `next_page_token` | Empty means the current page is the last page. | + +Continuation tokens are opaque and tied to the queue, change URI, and resolved request ID. This keeps every page on the same request even if a newer request becomes authoritative during traversal. A malformed token or a token reused with a different query is invalid. Callers may change `page_size` between pages. + +The project list does not change, so pagination does not skip or duplicate projects. Project results may appear while pages are being read; `project_results_complete`, not the contents of any one page, is the signal that every result is durable. + +## Storage Identity and Evolution + +URI, request ID, queue, and project identity use byte-exact comparison. 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 + +- 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 project list is not found. +- A URI mapping whose Request is not yet visible is unavailable and retryable; a mismatched Request, missing project list 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. + +## Rollout Phases + +The initial version returns an empty project list, `project_results_complete` set to false, and a repository degree of either 0 or 1. Once individual project-level mapping is available, project results and completion are included.