diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index b06da58..dd4e2d4 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -25,6 +25,8 @@ const ( VerbCompose = "compose" VerbComposeCandidate = "compose-candidate" VerbManifestReads = "manifest-reads" + VerbSeatList = "seat-list" + VerbSeatListAgain = "seat-list-again" ) const ( @@ -36,7 +38,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbHandOver, VerbAccessAct, VerbJudgeDirectories, VerbSetuidSearch, VerbRootFree, VerbActs, VerbPackages, VerbComposition, VerbCompose, VerbComposeCandidate, VerbManifestReads} + return []string{VerbHandOver, VerbAccessAct, VerbJudgeDirectories, VerbSetuidSearch, VerbRootFree, VerbActs, VerbPackages, VerbComposition, VerbCompose, VerbComposeCandidate, VerbManifestReads, VerbSeatList, VerbSeatListAgain} } // Events is every event published on the seat's subject: a new copy each call. @@ -70,6 +72,7 @@ const ( StateData = "data" StatePackages = "packages" StateHealth = "health" + StateFindings = "findings" ) // HaveByNodeModuleKey is the key of one value of the state have under the form ".". @@ -280,6 +283,37 @@ func DecodeHealthByNodeValue(raw []byte) (HealthByNodeValue, error) { return v, nil } +// FindingsByKeyKey is the key of one value of the state findings under the form "". +func FindingsByKeyKey(key string) string { return key } + +// ParseFindingsByKeyKey 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 ParseFindingsByKeyKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeFindingsByKeyValue reads one value of the state findings under the form "", refusing a field FindingsByKeyValue does not know and any text after the value. +func DecodeFindingsByKeyValue(raw []byte) (FindingsByKeyValue, error) { + var v FindingsByKeyValue + 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 one act asked through the seat, as acts reads it (ADR 0563 §9). type Act struct { // the act: hand-over, access-act, judge-directories or setuid-search @@ -371,6 +405,40 @@ type Asked struct { Says string `json:"says"` } +// FindingAction is one answer the operator may give from a notification. +type FindingAction struct { + Arguments map[string]string `json:"arguments,omitempty"` + Label string `json:"label"` + Level string `json:"level,omitempty"` + Machine string `json:"machine,omitempty"` + // the seat verb the operator channel calls + Verb string `json:"verb"` +} + +// The values of FindingAction's level. +const ( + FindingActionLevelAcknowledge = "acknowledge" + FindingActionLevelApprove = "approve" +) + +// FindingEvidence is one observation, as it was said. +type FindingEvidence struct { + At string `json:"at"` + Said string `json:"said"` +} + +// FindingSubject is what the condition is about. +type FindingSubject struct { + // the other machines it concerns + Also []string `json:"also,omitempty"` + // its id within the scope + ID string `json:"id"` + // the machine it concerns, when there is one + Machine string `json:"machine,omitempty"` + // machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module + Scope string `json:"scope"` +} + // ManifestReadsChanged is what a node-engine says when what it reads changed or its node returned (ADR 0618 §4). type ManifestReadsChanged struct { // when it says it, RFC 3339 @@ -450,6 +518,42 @@ type Said struct { Said string `json:"said"` } +// SeatListStatus is a type of the contract. +type SeatListStatus struct { + // the seat mesh-catalog has answered + Heard bool `json:"heard"` + Held []SeatListHeld `json:"held"` + LastError string `json:"last-error,omitempty"` + // a trunk definition that does not parse, by seat: the one held stands + Refused map[string]string `json:"refused,omitempty"` + Revision int64 `json:"revision,omitempty"` + // the seats held from the bootstrap alone + Waiting []string `json:"waiting,omitempty"` +} + +// SeatList is what an engine holds of the seat list (novox/hq ADR 0636 §7). +type SeatList struct { + // why the read failed, for seat-list-again + Error string `json:"error,omitempty"` + // the one line the engine says of it; empty when nothing waits + Health string `json:"health"` + Status SeatListStatus `json:"status"` +} + +// SeatListHeld is a type of the contract. +type SeatListHeld struct { + // the digest of the definition held; empty for a bootstrapped one + Digest string `json:"digest,omitempty"` + Seat string `json:"seat"` + Source string `json:"source"` +} + +// The values of SeatListHeld's source. +const ( + SeatListHeldSourceTrunk = "trunk" + SeatListHeldSourceBootstrap = "bootstrap" +) + // HandOverInput is what hand-over takes. type HandOverInput struct { // the directory's absolute path, as the engine states it @@ -773,6 +877,20 @@ type ManifestReadsAnswer struct { V int64 `json:"v"` } +// SeatListInput is what seat-list takes. +type SeatListInput struct { +} + +// SeatListAnswer is what seat-list answers. +type SeatListAnswer = SeatList + +// SeatListAgainInput is what seat-list-again takes. +type SeatListAgainInput struct { +} + +// SeatListAgainAnswer is what seat-list-again answers. +type SeatListAgainAnswer = SeatList + // AskedEvent is the body of the event asked. type AskedEvent = ActSaid @@ -885,3 +1003,40 @@ type HealthByNodeValue struct { Units map[string]any `json:"units,omitempty"` V int64 `json:"v"` } + +// FindingsByKeyValue is one value of the state findings under the key . +type FindingsByKeyValue struct { + Actions []FindingAction `json:"actions,omitempty"` + At string `json:"at"` + // newest first, at most 10 + Evidence []FindingEvidence `json:"evidence,omitempty"` + Explanation string `json:"explanation,omitempty"` + Headline string `json:"headline,omitempty"` + // scope.id.token + Key string `json:"key"` + Kind string `json:"kind"` + Needs string `json:"needs,omitempty"` + // the module that wrote it + Observer string `json:"observer"` + // the key's last part when it is not the kind + Ref string `json:"ref,omitempty"` + Resolved string `json:"resolved,omitempty"` + // self, operator, agent or healer: + Resolver string `json:"resolver,omitempty"` + // what of its own saw it last + Run string `json:"run,omitempty"` + Severity string `json:"severity"` + // the row, probe or event that saw it + Source string `json:"source"` + Subject FindingSubject `json:"subject"` + // one line + Summary string `json:"summary"` + // layout version, 1 + V int64 `json:"v"` +} + +// The values of FindingsByKeyValue's severity. +const ( + FindingsByKeyValueSeverityUrgent = "urgent" + FindingsByKeyValueSeverityWarning = "warning" +) diff --git a/go/seats/nodeengine/contract_test.go b/go/seats/nodeengine/contract_test.go index b1d7720..f54e529 100644 --- a/go/seats/nodeengine/contract_test.go +++ b/go/seats/nodeengine/contract_test.go @@ -35,6 +35,8 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.manifest-reads-changed\\.[A-Za-z0-9_-]+$")}, + {name: "the finding of a seat list unread, as the engine writes it", subject: "", given: "{\"at\":\"2026-11-05T14:00:00Z\",\"key\":\"machine.ace.seat-list-unread\",\"kind\":\"seat-list-unread\",\"observer\":\"mesh-host\",\"severity\":\"warning\",\"source\":\"node-engine.seat-list\",\"subject\":{\"id\":\"ace\",\"machine\":\"ace\",\"scope\":\"machine\"},\"summary\":\"the seat list has not been read for five minutes\",\"v\":1}", wire: "{\"at\":\"2026-11-05T14:00:00Z\",\"key\":\"machine.ace.seat-list-unread\",\"kind\":\"seat-list-unread\",\"observer\":\"mesh-host\",\"severity\":\"warning\",\"source\":\"node-engine.seat-list\",\"subject\":{\"id\":\"ace\",\"machine\":\"ace\",\"scope\":\"machine\"},\"summary\":\"the seat list has not been read for five minutes\",\"v\":1}", + decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads answers what the engine reads and when it stated it", subject: "", given: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", wire: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads of an engine built before it said them is the baseline", subject: "", given: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", wire: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", @@ -45,6 +47,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v ComposeCandidateAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "acts answers each act kept", subject: "", given: "{\"acts\":[{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"at\":\"2026-11-04T09:00:00Z\",\"caller\":\"mesh-tools\",\"expires\":\"2026-11-05T09:00:00Z\",\"id\":\"hand-over-1\",\"state\":\"asked\",\"subject\":\"/srv/media\"}],\"node\":\"ace\"}", wire: "{\"acts\":[{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"at\":\"2026-11-04T09:00:00Z\",\"caller\":\"mesh-tools\",\"expires\":\"2026-11-05T09:00:00Z\",\"id\":\"hand-over-1\",\"state\":\"asked\",\"subject\":\"/srv/media\"}],\"node\":\"ace\"}", decode: func(b []byte) (any, error) { var v ActsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "seat-list answers what the engine holds and what it waits for", subject: "", given: "{\"health\":\"the seat list is held from the bootstrap and nothing new is acted on\",\"status\":{\"heard\":false,\"held\":[{\"seat\":\"node-engine\",\"source\":\"bootstrap\"},{\"digest\":\"3f2a9c\",\"seat\":\"node-files\",\"source\":\"trunk\"}],\"last-error\":\"no answer\",\"waiting\":[\"node-engine\"]}}", wire: "{\"health\":\"the seat list is held from the bootstrap and nothing new is acted on\",\"status\":{\"heard\":false,\"held\":[{\"seat\":\"node-engine\",\"source\":\"bootstrap\"},{\"digest\":\"3f2a9c\",\"seat\":\"node-files\",\"source\":\"trunk\"}],\"last-error\":\"no answer\",\"waiting\":[\"node-engine\"]}}", + decode: func(b []byte) (any, error) { var v SeatListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "seat-list-again answers the error when the read failed", subject: "", given: "{\"error\":\"seatlist: no answer\",\"health\":\"the seat list is held from the bootstrap\",\"status\":{\"heard\":false,\"held\":[{\"seat\":\"node-engine\",\"source\":\"bootstrap\"}],\"waiting\":[\"node-engine\"]}}", wire: "{\"error\":\"seatlist: no answer\",\"health\":\"the seat list is held from the bootstrap\",\"status\":{\"heard\":false,\"held\":[{\"seat\":\"node-engine\",\"source\":\"bootstrap\"}],\"waiting\":[\"node-engine\"]}}", + decode: func(b []byte) (any, error) { var v SeatListAgainAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/seats/node-engine/fixtures/states.json b/seats/node-engine/fixtures/states.json new file mode 100644 index 0000000..2712dfd --- /dev/null +++ b/seats/node-engine/fixtures/states.json @@ -0,0 +1,17 @@ +[ + { + "name": "the finding of a seat list unread, as the engine writes it", + "of": "state:findings/", + "given": { + "v": 1, + "key": "machine.ace.seat-list-unread", + "kind": "seat-list-unread", + "subject": {"scope": "machine", "id": "ace", "machine": "ace"}, + "severity": "warning", + "summary": "the seat list has not been read for five minutes", + "source": "node-engine.seat-list", + "observer": "mesh-host", + "at": "2026-11-05T14:00:00Z" + } + } +] diff --git a/seats/node-engine/fixtures/verbs.json b/seats/node-engine/fixtures/verbs.json index 40fff31..3375279 100644 --- a/seats/node-engine/fixtures/verbs.json +++ b/seats/node-engine/fixtures/verbs.json @@ -72,5 +72,50 @@ } ] } + }, + { + "name": "seat-list answers what the engine holds and what it waits for", + "of": "verb:seat-list.answer", + "given": { + "status": { + "heard": false, + "held": [ + { + "seat": "node-engine", + "source": "bootstrap" + }, + { + "seat": "node-files", + "source": "trunk", + "digest": "3f2a9c" + } + ], + "waiting": [ + "node-engine" + ], + "last-error": "no answer" + }, + "health": "the seat list is held from the bootstrap and nothing new is acted on" + } + }, + { + "name": "seat-list-again answers the error when the read failed", + "of": "verb:seat-list-again.answer", + "given": { + "status": { + "heard": false, + "held": [ + { + "seat": "node-engine", + "source": "bootstrap" + } + ], + "waiting": [ + "node-engine" + ] + }, + "health": "the seat list is held from the bootstrap", + "error": "seatlist: no answer" + } } ] diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 20ed8a1..541d242 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -674,6 +674,30 @@ ], "description": "what this machine's engine reads of a manifest (ADR 0618 §3)" } + }, + { + "name": "seat-list", + "description": "The seat list this engine holds and what it waits for (novox/hq ADR 0636 §7): each seat's source (trunk or bootstrap) and digest, the seats held from the bootstrap alone, the definitions refused, the last error and whether the seat mesh-catalog has answered. Read-only.", + "callers": "the operator and the healers", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "$ref": "#/defs/SeatList" + } + }, + { + "name": "seat-list-again", + "description": "Make this engine read the seat list from the seat mesh-catalog now (novox/hq ADR 0636 §7), the recovery of a list unread; it answers what it holds afterwards, and the error when the read failed. A read that changed nothing still ends the engine's wait.", + "callers": "the operator and the healers", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "$ref": "#/defs/SeatList" + } } ], "events": [ @@ -1069,6 +1093,109 @@ } ], "description": "each node's health as its engine reports it" + }, + { + "name": "findings", + "template": "state", + "writer": "holder", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "one thing an observer sees wrong now", + "properties": { + "v": { + "type": "integer", + "description": "layout version, 1" + }, + "key": { + "type": "string", + "description": "scope.id.token" + }, + "kind": { + "type": "string" + }, + "subject": { + "$ref": "#/defs/FindingSubject" + }, + "severity": { + "type": "string", + "enum": [ + "urgent", + "warning" + ] + }, + "summary": { + "type": "string", + "description": "one line" + }, + "headline": { + "type": "string" + }, + "explanation": { + "type": "string" + }, + "resolved": { + "type": "string" + }, + "needs": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/defs/FindingAction" + } + }, + "evidence": { + "type": "array", + "description": "newest first, at most 10", + "items": { + "$ref": "#/defs/FindingEvidence" + } + }, + "source": { + "type": "string", + "description": "the row, probe or event that saw it" + }, + "resolver": { + "type": "string", + "description": "self, operator, agent or healer:" + }, + "ref": { + "type": "string", + "description": "the key's last part when it is not the kind" + }, + "observer": { + "type": "string", + "description": "the module that wrote it" + }, + "run": { + "type": "string", + "description": "what of its own saw it last" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "v", + "key", + "kind", + "subject", + "severity", + "summary", + "source", + "observer", + "at" + ] + }, + "description": "one finding of the observer that wrote it" + } + ], + "description": "the findings this node's engine raises about itself, one key per finding, in the layout of the seat mesh-conditions (its findings state): a key written raises the condition of the same key, a key deleted clears it (novox/hq ADR 0564 §2, ADR 0636 §1); today `machine..seat-list-unread`" } ], "defs": { @@ -1412,6 +1539,165 @@ "said" ], "description": "the answer of a read-only judgement" + }, + "SeatListHeld": { + "type": "object", + "properties": { + "seat": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "trunk", + "bootstrap" + ] + }, + "digest": { + "type": "string", + "description": "the digest of the definition held; empty for a bootstrapped one" + } + }, + "required": [ + "seat", + "source" + ] + }, + "SeatList": { + "type": "object", + "description": "what an engine holds of the seat list (novox/hq ADR 0636 §7)", + "properties": { + "status": { + "type": "object", + "properties": { + "heard": { + "type": "boolean", + "description": "the seat mesh-catalog has answered" + }, + "held": { + "type": "array", + "items": { + "$ref": "#/defs/SeatListHeld" + } + }, + "waiting": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the seats held from the bootstrap alone" + }, + "refused": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "a trunk definition that does not parse, by seat: the one held stands" + }, + "last-error": { + "type": "string" + }, + "revision": { + "type": "integer" + } + }, + "required": [ + "heard", + "held" + ] + }, + "health": { + "type": "string", + "description": "the one line the engine says of it; empty when nothing waits" + }, + "error": { + "type": "string", + "description": "why the read failed, for seat-list-again" + } + }, + "required": [ + "status", + "health" + ] + }, + "FindingSubject": { + "type": "object", + "description": "what the condition is about", + "properties": { + "scope": { + "type": "string", + "description": "machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module" + }, + "id": { + "type": "string", + "description": "its id within the scope" + }, + "machine": { + "type": "string", + "description": "the machine it concerns, when there is one" + }, + "also": { + "type": "array", + "description": "the other machines it concerns", + "items": { + "type": "string" + } + } + }, + "required": [ + "scope", + "id" + ] + }, + "FindingAction": { + "type": "object", + "description": "one answer the operator may give from a notification", + "properties": { + "label": { + "type": "string" + }, + "verb": { + "type": "string", + "description": "the seat verb the operator channel calls" + }, + "machine": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "level": { + "type": "string", + "enum": [ + "acknowledge", + "approve" + ] + } + }, + "required": [ + "label", + "verb" + ] + }, + "FindingEvidence": { + "type": "object", + "description": "one observation, as it was said", + "properties": { + "at": { + "type": "string", + "format": "date-time" + }, + "said": { + "type": "string" + } + }, + "required": [ + "at", + "said" + ] } } } diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index 39feb40..60a34dc 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -16,6 +16,8 @@ export const verbs = { compose: "compose", composeCandidate: "compose-candidate", manifestReads: "manifest-reads", + seatList: "seat-list", + seatListAgain: "seat-list-again", } as const; export const events = { @@ -49,6 +51,7 @@ export const states = { data: "data", packages: "packages", health: "health", + findings: "findings", } as const; /** The key of one value of the state have under the form ".". */ @@ -158,6 +161,21 @@ export function parseHealthByNodeKey(key: string): { node: string } | undefined return { node }; } +/** The key of one value of the state findings under the form "". */ +export function findingsByKeyKey(key: string): string { + return key; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseFindingsByKeyKey(key: string): { key: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const key = rest; + rest = ""; + if (rest !== "") return undefined; + return { key }; +} + /** one act asked through the seat, as acts reads it (ADR 0563 §9) */ export interface Act { /** the act: hand-over, access-act, judge-directories or setuid-search */ @@ -234,6 +252,34 @@ export interface Asked { "says": string; } +/** one answer the operator may give from a notification */ +export interface FindingAction { + "arguments"?: Record; + "label": string; + "level"?: "acknowledge" | "approve"; + "machine"?: string; + /** the seat verb the operator channel calls */ + "verb": string; +} + +/** one observation, as it was said */ +export interface FindingEvidence { + "at": string; + "said": string; +} + +/** what the condition is about */ +export interface FindingSubject { + /** the other machines it concerns */ + "also"?: string[]; + /** its id within the scope */ + "id": string; + /** the machine it concerns, when there is one */ + "machine"?: string; + /** machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module */ + "scope": string; +} + /** what a node-engine says when what it reads changed or its node returned (ADR 0618 §4) */ export interface ManifestReadsChanged { /** when it says it, RFC 3339 */ @@ -300,6 +346,34 @@ export interface Said { "said": string; } +export interface SeatListStatus { + /** the seat mesh-catalog has answered */ + "heard": boolean; + "held": SeatListHeld[]; + "last-error"?: string; + /** a trunk definition that does not parse, by seat: the one held stands */ + "refused"?: Record; + "revision"?: number; + /** the seats held from the bootstrap alone */ + "waiting"?: string[]; +} + +/** what an engine holds of the seat list (novox/hq ADR 0636 §7) */ +export interface SeatList { + /** why the read failed, for seat-list-again */ + "error"?: string; + /** the one line the engine says of it; empty when nothing waits */ + "health": string; + "status": SeatListStatus; +} + +export interface SeatListHeld { + /** the digest of the definition held; empty for a bootstrapped one */ + "digest"?: string; + "seat": string; + "source": "trunk" | "bootstrap"; +} + /** is what hand-over takes. */ export interface HandOverInput { /** the directory's absolute path, as the engine states it */ @@ -608,6 +682,20 @@ export interface ManifestReadsAnswer { "v": number; } +/** is what seat-list takes. */ +export interface SeatListInput { +} + +/** is what seat-list answers. */ +export type SeatListAnswer = SeatList; + +/** is what seat-list-again takes. */ +export interface SeatListAgainInput { +} + +/** is what seat-list-again answers. */ +export type SeatListAgainAnswer = SeatList; + /** is the body of the event asked. */ export type AskedEvent = ActSaid; @@ -720,3 +808,34 @@ export interface HealthByNodeValue { "v": number; } +/** is one value of the state findings under the key . */ +export interface FindingsByKeyValue { + "actions"?: FindingAction[]; + "at": string; + /** newest first, at most 10 */ + "evidence"?: FindingEvidence[]; + "explanation"?: string; + "headline"?: string; + /** scope.id.token */ + "key": string; + "kind": string; + "needs"?: string; + /** the module that wrote it */ + "observer": string; + /** the key's last part when it is not the kind */ + "ref"?: string; + "resolved"?: string; + /** self, operator, agent or healer: */ + "resolver"?: string; + /** what of its own saw it last */ + "run"?: string; + "severity": "urgent" | "warning"; + /** the row, probe or event that saw it */ + "source": string; + "subject": FindingSubject; + /** one line */ + "summary": string; + /** layout version, 1 */ + "v": number; +} +