diff --git a/contracts/go/clients/meshdeliveryclient/removed.go b/contracts/go/clients/meshdeliveryclient/removed.go deleted file mode 100644 index 8b390133..00000000 --- a/contracts/go/clients/meshdeliveryclient/removed.go +++ /dev/null @@ -1,18 +0,0 @@ -package meshdeliveryclient - -// RemovedPrefix opens the description of a verb release 6 removed from the seat mesh-delivery (novox/hq ADR 0539 clause 17): -// the definition keeps it as an optional row with no input, so the holder of before and the holder of release 6 both hold -// the seat, and a caller is answered by name with its replacement. -const RemovedPrefix = "Removed in R6 (ADR 0539): refused by name, naming its replacement: " - -// RemovedVerbs are the seat's verbs release 6 removed, each with its replacement in words: with no plan the gate keeps the -// merge's states and derives nothing after the merge. A new map each call. -func RemovedVerbs() map[string]string { - return map[string]string{ - "times": "the delivery view (nox-delivery): each machine's want and have, and since when", - "what-if": "the merge check's composition, which composes every machine a change touches", - "release": "the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", - "stop": "a bad fingerprint is held and its canary set back by the assigner (ADR 0429 ยง6, ADR 0539 clause 18)", - "retire-history": "nothing to retire: the gate keeps no delivery after its merge", - } -} diff --git a/contracts/go/seats/meshdelivery/contract.go b/contracts/go/seats/meshdelivery/contract.go index c08bb946..52e6cfb0 100644 --- a/contracts/go/seats/meshdelivery/contract.go +++ b/contracts/go/seats/meshdelivery/contract.go @@ -102,7 +102,9 @@ const SubjectRecoveryMerged = "mesh.seat.mesh-delivery.event.recovery-merged" const ( StateSource = "source" StateMerges = "merges" + StateAsks = "asks" StateOwed = "owed" + StateTurns = "turns" ) // SourceByBranchKey is the key of one value of the state source under the form "". @@ -167,6 +169,46 @@ func DecodeMergesByGroupValue(raw []byte) (MergesByGroupValue, error) { return v, nil } +// AsksByMergeGroupHeadsKey is the key of one value of the state asks under the form "merge..". +func AsksByMergeGroupHeadsKey(group, heads string) string { return "merge." + group + "." + heads } + +// ParseAsksByMergeGroupHeadsKey reads the parts of a key of the form "merge..", in the order of the form; the last result is false for a key of another form. +func ParseAsksByMergeGroupHeadsKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "merge.") { + return "", "", false + } + rest = rest[6:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodeAsksByMergeGroupHeadsValue reads one value of the state asks under the form "merge..", refusing a field AsksByMergeGroupHeadsValue does not know and any text after the value. +func DecodeAsksByMergeGroupHeadsValue(raw []byte) (AsksByMergeGroupHeadsValue, error) { + var v AsksByMergeGroupHeadsValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // OwedByRepositoryNumberHeadActKey is the key of one value of the state owed under the form "...". func OwedByRepositoryNumberHeadActKey(repository, number, head, act string) string { return repository + "." + number + "." + head + "." + act @@ -215,6 +257,37 @@ func DecodeOwedByRepositoryNumberHeadActValue(raw []byte) (OwedByRepositoryNumbe return v, nil } +// TurnsByRepositoryKey is the key of one value of the state turns under the form "". +func TurnsByRepositoryKey(repository string) string { return repository } + +// ParseTurnsByRepositoryKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseTurnsByRepositoryKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeTurnsByRepositoryValue reads one value of the state turns under the form "", refusing a field TurnsByRepositoryValue does not know and any text after the value. +func DecodeTurnsByRepositoryValue(raw []byte) (TurnsByRepositoryValue, error) { + var v TurnsByRepositoryValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // Act is an act the gate said for a group, with its recorded outcome once the forge bridge said one. type Act struct { // when the outcome was recorded @@ -595,6 +668,27 @@ type MergesByGroupValue struct { V int64 `json:"v,omitempty"` } +// AsksByMergeGroupHeadsValueHeadsItem is a type of the contract. +type AsksByMergeGroupHeadsValueHeadsItem struct { + // the full commit + Head string `json:"head"` + Number int64 `json:"number"` + // / + Repository string `json:"repository"` +} + +// AsksByMergeGroupHeadsValue is one value of the state asks under the key merge... +type AsksByMergeGroupHeadsValue struct { + // the operator-channel ask as raised + Ask any `json:"ask"` + // the number of times it was asked + Asking int64 `json:"asking,omitempty"` + Group string `json:"group"` + Heads []AsksByMergeGroupHeadsValueHeadsItem `json:"heads"` + // the layout version + V int64 `json:"v,omitempty"` +} + // OwedByRepositoryNumberHeadActValue is one value of the state owed under the key .... type OwedByRepositoryNumberHeadActValue struct { // status=, view, note or comment. @@ -607,3 +701,26 @@ type OwedByRepositoryNumberHeadActValue struct { // the layout version V int64 `json:"v,omitempty"` } + +// TurnsByRepositoryValueWaitingItem is a type of the contract. +type TurnsByRepositoryValueWaitingItem struct { + // when (RFC 3339 time) + Approved time.Time `json:"approved"` + Ask string `json:"ask,omitempty"` + Group string `json:"group"` + Heads string `json:"heads"` + // when (RFC 3339 time) + Since time.Time `json:"since"` + Step string `json:"step,omitempty"` + WaitsFor string `json:"waits-for,omitempty"` + // when (RFC 3339 time) + WaitsSince *time.Time `json:"waits-since,omitempty"` +} + +// TurnsByRepositoryValue is one value of the state turns under the key . +type TurnsByRepositoryValue struct { + Repository string `json:"repository"` + // the layout version + V int64 `json:"v,omitempty"` + Waiting []TurnsByRepositoryValueWaitingItem `json:"waiting"` +} diff --git a/contracts/go/seats/meshdelivery/contract_test.go b/contracts/go/seats/meshdelivery/contract_test.go index f4cf7b7c..0dab3001 100644 --- a/contracts/go/seats/meshdelivery/contract_test.go +++ b/contracts/go/seats/meshdelivery/contract_test.go @@ -53,6 +53,14 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a turn", subject: "", given: "{\"repository\":\"novox/mesh-sdk\",\"v\":1,\"waiting\":[{\"approved\":\"2026-11-05T10:00:00Z\",\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"since\":\"2026-11-05T10:01:00Z\"}]}", wire: "{\"repository\":\"novox/mesh-sdk\",\"v\":1,\"waiting\":[{\"approved\":\"2026-11-05T10:00:00Z\",\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"since\":\"2026-11-05T10:01:00Z\"}]}", + decode: func(b []byte) (any, error) { var v TurnsByRepositoryValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "an ask", subject: "", given: "{\"ask\":{\"headline\":\"merge\"},\"group\":\"feat/registry\",\"heads\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}", wire: "{\"ask\":{\"headline\":\"merge\"},\"group\":\"feat/registry\",\"heads\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}", + decode: func(b []byte) (any, error) { + var v AsksByMergeGroupHeadsValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "groups asks for one group", subject: "", given: "{\"group\":\"feat/47-G\"}", wire: "{\"group\":\"feat/47-G\"}", decode: func(b []byte) (any, error) { var v GroupsInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "groups answers a group with its members and acts", subject: "", given: "{\"groups\":[{\"acts\":[{\"at\":\"2026-11-09T20:01:00Z\",\"outcome\":\"performed\",\"owed\":{\"act\":\"merge\",\"ask\":\"merge.feat=47-G.0123456789ab\",\"group\":\"feat/47-G\",\"head\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"id\":\"merge.feat=47-G.0123456789ab.novox/mesh-sdk#178\",\"number\":178,\"repository\":\"novox/mesh-sdk\",\"said\":\"2026-11-09T20:00:00Z\"}},{\"owed\":{\"act\":\"status\",\"context\":\"mesh/merge-gate\",\"description\":\"waits for the operator's Approve\",\"head\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"id\":\"status.novox/mesh-sdk#178.aaaaaaaaaaaa.mesh/merge-gate\",\"number\":178,\"repository\":\"novox/mesh-sdk\",\"state\":\"pending\",\"target\":\"https://forge.test/x\"}}],\"check\":\"pass\",\"id\":\"feat/47-G\",\"members\":[{\"base\":\"main\",\"head\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"id\":\"novox/mesh-sdk#178\",\"number\":178,\"open\":true,\"repository\":\"novox/mesh-sdk\",\"state\":\"merging\"}],\"state\":\"merging\",\"why\":\"novox/mesh-sdk#178 merges first\"}]}", wire: "{\"groups\":[{\"acts\":[{\"at\":\"2026-11-09T20:01:00Z\",\"outcome\":\"performed\",\"owed\":{\"act\":\"merge\",\"ask\":\"merge.feat=47-G.0123456789ab\",\"group\":\"feat/47-G\",\"head\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"id\":\"merge.feat=47-G.0123456789ab.novox/mesh-sdk#178\",\"number\":178,\"repository\":\"novox/mesh-sdk\",\"said\":\"2026-11-09T20:00:00Z\"}},{\"owed\":{\"act\":\"status\",\"context\":\"mesh/merge-gate\",\"description\":\"waits for the operator's Approve\",\"head\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"id\":\"status.novox/mesh-sdk#178.aaaaaaaaaaaa.mesh/merge-gate\",\"number\":178,\"repository\":\"novox/mesh-sdk\",\"state\":\"pending\",\"target\":\"https://forge.test/x\"}}],\"check\":\"pass\",\"id\":\"feat/47-G\",\"members\":[{\"base\":\"main\",\"head\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"id\":\"novox/mesh-sdk#178\",\"number\":178,\"open\":true,\"repository\":\"novox/mesh-sdk\",\"state\":\"merging\"}],\"state\":\"merging\",\"why\":\"novox/mesh-sdk#178 merges first\"}]}", diff --git a/contracts/seats/mesh-delivery/fixtures/states.json b/contracts/seats/mesh-delivery/fixtures/states.json index b8f7dd9b..78776547 100644 --- a/contracts/seats/mesh-delivery/fixtures/states.json +++ b/contracts/seats/mesh-delivery/fixtures/states.json @@ -34,5 +34,39 @@ "v": 1, "act": "view" } + }, + { + "name": "a turn", + "of": "state:turns/", + "given": { + "v": 1, + "repository": "novox/mesh-sdk", + "waiting": [ + { + "group": "feat/registry", + "heads": "0123456789ab", + "approved": "2026-11-05T10:00:00Z", + "since": "2026-11-05T10:01:00Z" + } + ] + } + }, + { + "name": "an ask", + "of": "state:asks/merge..", + "given": { + "v": 1, + "group": "feat/registry", + "heads": [ + { + "repository": "novox/mesh-sdk", + "number": 1, + "head": "cccccccccccccccccccccccccccccccccccccccc" + } + ], + "ask": { + "headline": "merge" + } + } } ] diff --git a/contracts/seats/mesh-delivery/seat.json b/contracts/seats/mesh-delivery/seat.json index 1dd1e613..4f4bf39f 100644 --- a/contracts/seats/mesh-delivery/seat.json +++ b/contracts/seats/mesh-delivery/seat.json @@ -567,6 +567,66 @@ "description": "the merges", "history": 10 }, + { + "name": "asks", + "template": "state", + "key-forms": [ + { + "key": "merge..", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "group": { + "type": "string" + }, + "heads": { + "type": "array", + "items": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "/" + }, + "number": { + "type": "integer" + }, + "head": { + "type": "string", + "description": "the full commit" + } + }, + "required": [ + "repository", + "number", + "head" + ] + } + }, + "ask": { + "description": "the operator-channel ask as raised" + }, + "asking": { + "type": "integer", + "description": "the number of times it was asked" + } + }, + "required": [ + "group", + "heads", + "ask" + ] + }, + "description": "the ask of the operator for one group at one set of heads" + } + ], + "description": "the asks of the operator for a merge", + "history": 5 + }, { "name": "owed", "template": "state", @@ -610,6 +670,79 @@ ], "description": "the acts the gate owes the forge", "history": 5 + }, + { + "name": "turns", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "repository": { + "type": "string" + }, + "waiting": { + "type": "array", + "items": { + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "heads": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "approved": { + "type": "string", + "format": "date-time", + "description": "when (RFC 3339 time)" + }, + "since": { + "type": "string", + "format": "date-time", + "description": "when (RFC 3339 time)" + }, + "waits-for": { + "type": "string" + }, + "waits-since": { + "type": "string", + "format": "date-time", + "description": "when (RFC 3339 time)" + }, + "step": { + "type": "string" + } + }, + "required": [ + "group", + "heads", + "approved", + "since" + ] + } + } + }, + "required": [ + "repository", + "waiting" + ], + "description": "the merges of a repository in the order they wait for their turn" + }, + "description": "the merges waiting on a repository" + } + ], + "description": "which merge waits for which", + "history": 5 } ], "defs": { diff --git a/contracts/ts/seats/meshdelivery/index.d.ts b/contracts/ts/seats/meshdelivery/index.d.ts index 18e010be..c00f97c5 100644 --- a/contracts/ts/seats/meshdelivery/index.d.ts +++ b/contracts/ts/seats/meshdelivery/index.d.ts @@ -60,7 +60,9 @@ export declare const recoveryMergedSubject: "mesh.seat.mesh-delivery.event.recov export declare const states: { readonly source: "source"; readonly merges: "merges"; + readonly asks: "asks"; readonly owed: "owed"; + readonly turns: "turns"; }; /** The key of one value of the state source under the form "". */ @@ -75,12 +77,24 @@ export declare function mergesByGroupKey(group: string): string; /** The parts of a key of the form "", or undefined for a key of another form. */ export declare function parseMergesByGroupKey(_key: string): { group: string } | undefined; +/** The key of one value of the state asks under the form "merge..". */ +export declare function asksByMergeGroupHeadsKey(group: string, heads: string): string; + +/** The parts of a key of the form "merge..", or undefined for a key of another form. */ +export declare function parseAsksByMergeGroupHeadsKey(_key: string): { group: string; heads: string } | undefined; + /** The key of one value of the state owed under the form "...". */ export declare function owedByRepositoryNumberHeadActKey(repository: string, number: string, head: string, act: string): string; /** The parts of a key of the form "...", or undefined for a key of another form. */ export declare function parseOwedByRepositoryNumberHeadActKey(_key: string): { repository: string; number: string; head: string; act: string } | undefined; +/** The key of one value of the state turns under the form "". */ +export declare function turnsByRepositoryKey(repository: string): string; + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export declare function parseTurnsByRepositoryKey(_key: string): { repository: string } | undefined; + /** an act the gate said for a group, with its recorded outcome once the forge bridge said one */ export interface Act { /** when the outcome was recorded */ @@ -429,6 +443,26 @@ export interface MergesByGroupValue { "v"?: number; } +export interface AsksByMergeGroupHeadsValueHeadsItem { + /** the full commit */ + "head": string; + "number": number; + /** / */ + "repository": string; +} + +/** is one value of the state asks under the key merge... */ +export interface AsksByMergeGroupHeadsValue { + /** the operator-channel ask as raised */ + "ask": unknown; + /** the number of times it was asked */ + "asking"?: number; + "group": string; + "heads": AsksByMergeGroupHeadsValueHeadsItem[]; + /** the layout version */ + "v"?: number; +} + /** is one value of the state owed under the key .... */ export interface OwedByRepositoryNumberHeadActValue { /** status=, view, note or comment. */ @@ -442,3 +476,25 @@ export interface OwedByRepositoryNumberHeadActValue { "v"?: number; } +export interface TurnsByRepositoryValueWaitingItem { + /** when (RFC 3339 time) */ + "approved": string; + "ask"?: string; + "group": string; + "heads": string; + /** when (RFC 3339 time) */ + "since": string; + "step"?: string; + "waits-for"?: string; + /** when (RFC 3339 time) */ + "waits-since"?: string; +} + +/** is one value of the state turns under the key . */ +export interface TurnsByRepositoryValue { + "repository": string; + /** the layout version */ + "v"?: number; + "waiting": TurnsByRepositoryValueWaitingItem[]; +} + diff --git a/contracts/ts/seats/meshdelivery/index.js b/contracts/ts/seats/meshdelivery/index.js index b2519685..b3eed1fb 100644 --- a/contracts/ts/seats/meshdelivery/index.js +++ b/contracts/ts/seats/meshdelivery/index.js @@ -64,7 +64,9 @@ export const recoveryMergedSubject = "mesh.seat.mesh-delivery.event.recovery-mer export const states = { source: "source", merges: "merges", + asks: "asks", owed: "owed", + turns: "turns", }; /** The key of one value of the state source under the form "". */ @@ -97,6 +99,27 @@ export function parseMergesByGroupKey(_key) { return { group }; } +/** The key of one value of the state asks under the form "merge..". */ +export function asksByMergeGroupHeadsKey(group, heads) { + return "merge." + group + "." + heads; +} + +/** The parts of a key of the form "merge..", or undefined for a key of another form. */ +export function parseAsksByMergeGroupHeadsKey(_key) { + let _rest = _key; + if (!_rest.startsWith("merge.")) return undefined; + _rest = _rest.slice(6); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const group = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + if (_rest === "") return undefined; + const heads = _rest; + _rest = ""; + if (_rest !== "") return undefined; + return { group, heads }; +} + /** The key of one value of the state owed under the form "...". */ export function owedByRepositoryNumberHeadActKey(repository, number, head, act) { return repository + "." + number + "." + head + "." + act; @@ -124,3 +147,18 @@ export function parseOwedByRepositoryNumberHeadActKey(_key) { return { repository, number, head, act }; } +/** The key of one value of the state turns under the form "". */ +export function turnsByRepositoryKey(repository) { + return repository; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseTurnsByRepositoryKey(_key) { + let _rest = _key; + if (_rest === "") return undefined; + const repository = _rest; + _rest = ""; + if (_rest !== "") return undefined; + return { repository }; +} +