diff --git a/internal/cli/alert.go b/internal/cli/alert.go index d8decf5..57b09cd 100644 --- a/internal/cli/alert.go +++ b/internal/cli/alert.go @@ -120,7 +120,7 @@ func newAlertListCmd() *cobra.Command { cmd.Flags().BoolVar(&muted, "muted", false, "Show ever-muted only") cmd.Flags().StringVar(&since, "since", "24h", "Start time") cmd.Flags().StringVar(&until, "until", "now", "End time") - cmd.Flags().IntVar(&limit, "limit", 20, "Max results") + cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)") cmd.Flags().IntVar(&page, "page", 1, "Page number") cmd.Flags().StringVar(&fields, "fields", "", "Comma-separated fields to project in json/toon output (e.g. alert_id,title,alert_severity,created_at); ignored in table mode. Use to avoid dumping the full nested record.") diff --git a/internal/cli/alert_event.go b/internal/cli/alert_event.go index f510ccf..fc23434 100644 --- a/internal/cli/alert_event.go +++ b/internal/cli/alert_event.go @@ -90,6 +90,8 @@ func newAlertEventListCmd() *cobra.Command { fieldNames := []string{"event_id", "alert_id", "event_severity", "event_status", "event_time", "title"} if fields != "" { fieldNames = parseStringSlice(fields) + } else { + noteDefaultProjection(cmd.ErrOrStderr(), fieldNames) } proj, err := projectFields(result.Items, fieldNames) if err != nil { @@ -113,7 +115,7 @@ func newAlertEventListCmd() *cobra.Command { cmd.Flags().StringVar(&integrationType, "integration-type", "", "Comma-separated integration types (plugin keys, e.g. AliCloud,Prometheus) — not integration IDs; use --integration for that") cmd.Flags().StringVar(&since, "since", "1h", "Start time") cmd.Flags().StringVar(&until, "until", "now", "End time") - cmd.Flags().IntVar(&limit, "limit", 20, "Max results") + cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)") cmd.Flags().IntVar(&page, "page", 1, "Page number") cmd.Flags().StringVar(&fields, "fields", "", "Comma-separated fields to project in json/toon output (e.g. event_id,alert_id,event_severity,event_status,event_time,title); ignored in table mode. Defaults to these compact event fields. Long strings are truncated as needed to keep structured output below 16 KiB.") diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go index 08c95d0..9754dce 100644 --- a/internal/cli/command_test.go +++ b/internal/cli/command_test.go @@ -87,6 +87,28 @@ func execCommand(args ...string) (string, error) { return buf.String(), err } +// execCommandSplit is execCommand with stdout and stderr captured separately, +// for tests that assert machine-readable stdout stays pure while advisory +// notices (e.g. the default-projection note) land on stderr. +func execCommandSplit(args ...string) (stdout, stderr string, err error) { + resetCommandFlags(rootCmd) + + outBuf := new(bytes.Buffer) + errBuf := new(bytes.Buffer) + rootCmd.SetOut(outBuf) + rootCmd.SetErr(errBuf) + rootCmd.SetArgs(args) + + err = rootCmd.Execute() + + rootCmd.SetArgs(nil) + rootCmd.SetOut(nil) + rootCmd.SetErr(nil) + resetCommandFlags(rootCmd) + + return outBuf.String(), errBuf.String(), err +} + func resetCommandFlags(cmd *cobra.Command) { if cmd == nil { return diff --git a/internal/cli/fieldproject.go b/internal/cli/fieldproject.go index 5c2d31e..b754875 100644 --- a/internal/cli/fieldproject.go +++ b/internal/cli/fieldproject.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "io" "reflect" "sort" "strings" @@ -69,6 +70,16 @@ func projectFields(items any, fields []string) ([]map[string]any, error) { return out, nil } +// noteDefaultProjection announces on stderr that structured rows were reduced +// to the command's compact default projection. Without it, a reader piping +// stdout to jq sees an unselected key (labels, description, …) as null on +// every row and can conclude the server never returns it, when it is one +// --fields away. stderr keeps stdout byte-identical for jq/toon pipelines. +func noteDefaultProjection(w io.Writer, fields []string) { + _, _ = fmt.Fprintf(w, "note: rows projected to default compact fields (%s); other response fields are available via --fields\n", + strings.Join(fields, ",")) +} + // boundProjectedOutput keeps the new agent-oriented projections below their // command budget without changing the selected keys. List rows (many small // records) are shortened fairly when they overflow the budget, with diff --git a/internal/cli/fieldproject_test.go b/internal/cli/fieldproject_test.go index 4ebdb77..3437219 100644 --- a/internal/cli/fieldproject_test.go +++ b/internal/cli/fieldproject_test.go @@ -213,12 +213,15 @@ func TestIncidentListStructuredDefaultUsesCompactProjection(t *testing.T) { stub := newGFStub(t) stub.data = map[string]any{"items": []any{incidentRow()}, "total": 1} - out, err := execCommand("incident", "list", "--output-format", "json") + out, stderrText, err := execCommandSplit("incident", "list", "--output-format", "json") if err != nil { - t.Fatalf("execCommand: %v", err) + t.Fatalf("execCommandSplit: %v", err) } assertProjectedJSONFields(t, out, []string{"incident_id", "title", "incident_severity", "progress", "start_time", "channel_id"}) + if !strings.Contains(stderrText, "note: rows projected to default compact fields") { + t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText) + } }) t.Run("toon default", func(t *testing.T) { @@ -226,16 +229,21 @@ func TestIncidentListStructuredDefaultUsesCompactProjection(t *testing.T) { stub := newGFStub(t) stub.data = map[string]any{"items": []any{incidentRow()}, "total": 1} - out, err := execCommand("incident", "list", "--output-format", "toon") + out, stderrText, err := execCommandSplit("incident", "list", "--output-format", "toon") if err != nil { - t.Fatalf("execCommand: %v", err) + t.Fatalf("execCommandSplit: %v", err) } + // Positive keys must come from stdout alone: the stderr note embeds the + // same field names, so a merged capture would satisfy this vacuously. for _, key := range []string{"incident_id", "title", "incident_severity", "progress", "start_time", "channel_id"} { if !strings.Contains(out, key) { t.Errorf("default toon output missing compact key %q, got:\n%s", key, out) } } + if !strings.Contains(stderrText, "note: rows projected to default compact fields") { + t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText) + } for _, key := range []string{"responders", "labels", "description"} { if strings.Contains(out, key) { t.Errorf("default toon output should not contain full-record key %q, got:\n%s", key, out) @@ -489,13 +497,16 @@ func TestIncidentSimilarStructuredProjection(t *testing.T) { } stub.data = map[string]any{"items": items, "total": len(items)} - out, err := execCommand("incident", "similar", "inc-1", "--limit", "20", "--output-format", "json") + out, stderrText, err := execCommandSplit("incident", "similar", "inc-1", "--limit", "20", "--output-format", "json") if err != nil { - t.Fatalf("execCommand: %v", err) + t.Fatalf("execCommandSplit: %v", err) } if len(out) >= 16*1024 { t.Fatalf("compact similar output is %d bytes, want <16 KiB", len(out)) } + if !strings.Contains(stderrText, "note: rows projected to default compact fields") { + t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText) + } var rows []map[string]json.RawMessage if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &rows); err != nil { @@ -614,13 +625,16 @@ func TestAlertEventListStructuredProjection(t *testing.T) { } stub.data = map[string]any{"items": items, "total": len(items)} - out, err := execCommand("alert-event", "list", "--limit", "30", "--output-format", "json") + out, stderrText, err := execCommandSplit("alert-event", "list", "--limit", "30", "--output-format", "json") if err != nil { - t.Fatalf("execCommand: %v", err) + t.Fatalf("execCommandSplit: %v", err) } if len(out) >= 16*1024 { t.Fatalf("compact alert-event output is %d bytes, want <16 KiB", len(out)) } + if !strings.Contains(stderrText, "note: rows projected to default compact fields") { + t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText) + } var rows []map[string]json.RawMessage if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &rows); err != nil { t.Fatalf("parse compact alert-event json: %v\n%s", err, out) diff --git a/internal/cli/incident.go b/internal/cli/incident.go index 319c503..0665d91 100644 --- a/internal/cli/incident.go +++ b/internal/cli/incident.go @@ -120,6 +120,8 @@ func newIncidentListCmd() *cobra.Command { if len(selectedFields) == 0 { return fmt.Errorf("--fields must name at least one field") } + } else { + noteDefaultProjection(cmd.ErrOrStderr(), selectedFields) } proj, err := projectFields(result.Items, selectedFields) if err != nil { @@ -605,6 +607,8 @@ func newIncidentSimilarCmd() *cobra.Command { fieldNames := []string{"incident_id", "title", "incident_severity", "progress", "start_time", "close_time", "ack_time", "alert_cnt", "root_cause", "score"} if fields != "" { fieldNames = parseStringSlice(fields) + } else { + noteDefaultProjection(cmd.ErrOrStderr(), fieldNames) } proj, err := projectFields(result.Items, fieldNames) if err != nil { diff --git a/internal/cli/insight.go b/internal/cli/insight.go index f3b4a5f..a2e02bd 100644 --- a/internal/cli/insight.go +++ b/internal/cli/insight.go @@ -138,7 +138,7 @@ func newInsightIncidentsCmd() *cobra.Command { cmd.Flags().StringVar(&since, "since", "7d", "Start time") cmd.Flags().StringVar(&until, "until", "now", "End time") - cmd.Flags().IntVar(&limit, "limit", 20, "Max results") + cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)") cmd.Flags().IntVar(&page, "page", 1, "Page number") return cmd diff --git a/internal/cli/zz_generated_alerts.go b/internal/cli/zz_generated_alerts.go index 3dcf5ed..4eb3fd0 100644 --- a/internal/cli/zz_generated_alerts.go +++ b/internal/cli/zz_generated_alerts.go @@ -794,13 +794,19 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le - creator_id (integer) — Member ID who created the pipeline. - integration_id (integer) — Integration ID this pipeline applies to. - rules (array) — Ordered list of processing rules. - - if (array) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts. + - if (array>) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts. + - key (string) (required) — Field name to filter on. Use plain names for built-in alert fields (e.g. 'alert_severity', 'alert_key', 'check', 'resource', 'service', 'cluster') or the 'labels.' prefix for custom alert labels (e.g. 'labels.env', 'labels.region'). + - oper (string) (required) — Filter operator. 'IN' — value must match one of 'vals'; 'NOTIN' — value must not match any of 'vals'. Supports regex patterns wrapped in '/pattern/'. [IN, NOTIN] + - vals (array) (required) — List of values to match against. Each entry is a plain string or a '/regex/' pattern. - kind (string) — Rule type. [title_reset, description_reset, severity_reset, alert_drop, alert_inhibit] - settings (object) — Kind-specific settings. Shape depends on 'kind': - 'title_reset': '{ "title": "" }' - 'description_reset': '{ "description": "" }' - 'severity_reset': '{ "severity": "Critical"|"Warning"|"Info" }' - 'alert_drop': '{}' (empty object) - 'alert_inhibit': '{ "equals": ["", ...], "source_filters": }' - description (string) — New description template. - equals (array) — Label keys whose values must be equal between the source and current alert for inhibition to apply. - severity (string) — Target severity level. [Critical, Warning, Info] - - source_filters (array) — Filter that identifies the source alerts to inhibit. + - source_filters (array>) — Filter that identifies the source alerts to inhibit. + - key (string) (required) — Field name to filter on. Use plain names for built-in alert fields (e.g. 'alert_severity', 'alert_key', 'check', 'resource', 'service', 'cluster') or the 'labels.' prefix for custom alert labels (e.g. 'labels.env', 'labels.region'). + - oper (string) (required) — Filter operator. 'IN' — value must match one of 'vals'; 'NOTIN' — value must not match any of 'vals'. Supports regex patterns wrapped in '/pattern/'. [IN, NOTIN] + - vals (array) (required) — List of values to match against. Each entry is a plain string or a '/regex/' pattern. - title (string) — New title template. Supports Golang template syntax referencing alert fields. - status (string) — Pipeline status. Possible values: 'enabled', 'disabled'. - updated_at (integer) — Last update timestamp, Unix epoch seconds. @@ -860,13 +866,16 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; - creator_id (integer) — Member ID who created the pipeline. - integration_id (integer) — Integration ID this pipeline applies to. - rules (array) — Ordered list of processing rules. - - if (array) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts. + - if (array>) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts. + - key (string) (required) — Field name to filter on. Use plain names for built-in alert fields (e.g. 'alert_severity', 'alert_key', 'check', 'resource', 'service', 'cluster') or the 'labels.' prefix for custom alert labels (e.g. 'labels.env', 'labels.region'). + - oper (string) (required) — Filter operator. 'IN' — value must match one of 'vals'; 'NOTIN' — value must not match any of 'vals'. Supports regex patterns wrapped in '/pattern/'. [IN, NOTIN] + - vals (array) (required) — List of values to match against. Each entry is a plain string or a '/regex/' pattern. - kind (string) — Rule type. [title_reset, description_reset, severity_reset, alert_drop, alert_inhibit] - settings (object) — Kind-specific settings. Shape depends on 'kind': - 'title_reset': '{ "title": "" }' - 'description_reset': '{ "description": "" }' - 'severity_reset': '{ "severity": "Critical"|"Warning"|"Info" }' - 'alert_drop': '{}' (empty object) - 'alert_inhibit': '{ "equals": ["", ...], "source_filters": }' - description (string) — New description template. - equals (array) — Label keys whose values must be equal between the source and current alert for inhibition to apply. - severity (string) — Target severity level. [Critical, Warning, Info] - - source_filters (array) — Filter that identifies the source alerts to inhibit. + - source_filters (array>) — Filter that identifies the source alerts to inhibit. - title (string) — New title template. Supports Golang template syntax referencing alert fields. - status (string) — Pipeline status. Possible values: 'enabled', 'disabled'. - updated_at (integer) — Last update timestamp, Unix epoch seconds. @@ -996,13 +1005,19 @@ API: POST /alert/pipeline/upsert (alert-write-pipeline-upsert) Request fields: --integration-id int (required) — Integration ID to configure. rules (array, via --data) (required) — Rules to apply. Max 50. - - if (array) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts. + - if (array>) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts. + - key (string) (required) — Field name to filter on. Use plain names for built-in alert fields (e.g. 'alert_severity', 'alert_key', 'check', 'resource', 'service', 'cluster') or the 'labels.' prefix for custom alert labels (e.g. 'labels.env', 'labels.region'). + - oper (string) (required) — Filter operator. 'IN' — value must match one of 'vals'; 'NOTIN' — value must not match any of 'vals'. Supports regex patterns wrapped in '/pattern/'. [IN, NOTIN] + - vals (array) (required) — List of values to match against. Each entry is a plain string or a '/regex/' pattern. - kind (string) — Rule type. [title_reset, description_reset, severity_reset, alert_drop, alert_inhibit] - settings (object) — Kind-specific settings. Shape depends on 'kind': - 'title_reset': '{ "title": "" }' - 'description_reset': '{ "description": "" }' - 'severity_reset': '{ "severity": "Critical"|"Warning"|"Info" }' - 'alert_drop': '{}' (empty object) - 'alert_inhibit': '{ "equals": ["", ...], "source_filters": }' - description (string) — New description template. - equals (array) — Label keys whose values must be equal between the source and current alert for inhibition to apply. - severity (string) — Target severity level. [Critical, Warning, Info] - - source_filters (array) — Filter that identifies the source alerts to inhibit. + - source_filters (array>) — Filter that identifies the source alerts to inhibit. + - key (string) (required) — Field name to filter on. Use plain names for built-in alert fields (e.g. 'alert_severity', 'alert_key', 'check', 'resource', 'service', 'cluster') or the 'labels.' prefix for custom alert labels (e.g. 'labels.env', 'labels.region'). + - oper (string) (required) — Filter operator. 'IN' — value must match one of 'vals'; 'NOTIN' — value must not match any of 'vals'. Supports regex patterns wrapped in '/pattern/'. [IN, NOTIN] + - vals (array) (required) — List of values to match against. Each entry is a plain string or a '/regex/' pattern. - title (string) — New title template. Supports Golang template syntax referencing alert fields. `, Args: requireBodyFieldOrExactArg("integration_id", "integration-id"), diff --git a/internal/cli/zz_generated_channels.go b/internal/cli/zz_generated_channels.go index 07c6163..d89290d 100644 --- a/internal/cli/zz_generated_channels.go +++ b/internal/cli/zz_generated_channels.go @@ -66,7 +66,7 @@ Request fields: group (object, via --data) — Alert grouping configuration. - all_equals_required (boolean) — When true, all listed keys must be present for grouping. - cases (array) — Per-filter grouping overrides. - - equals (array) — Groups of label keys whose equality defines a bucket. + - equals (array>) — Groups of label keys whose equality defines a bucket. - i_keys (array) — Label keys used for intelligent grouping embeddings. - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1) - method (string) (required) — Grouping method: 'i' intelligent, 'p' pattern, 'n' none. [i, p, n] @@ -329,7 +329,10 @@ Request fields: --priority int — Evaluation priority. Lower runs first. (0-200) --rule-name string (required) — Rule name, 1 to 39 characters. (1-39 chars) --template-id string (required) — Notification template ID (MongoDB ObjectID). - filters (array, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + filters (array>, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + - key (string) (required) — Field key (e.g. 'alert_severity', 'labels.service'). + - oper (string) (required) — Filter operator. [IN, NOTIN] + - vals (array) (required) — Values to match. layers (array, via --data) (required) — Escalation levels in order. At least one level is required. - escalate_window (integer) — Wait before moving to the next level, in minutes. (0-720) - force_escalate (boolean) — When true, always escalate regardless of acknowledgement. @@ -892,7 +895,7 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le - group (object) — Alert grouping configuration. - all_equals_required (boolean) — When true, all listed keys must be present for grouping. - cases (array) — Per-filter grouping overrides. - - equals (array) — Groups of label keys whose equality defines a bucket. + - equals (array>) — Groups of label keys whose equality defines a bucket. - i_keys (array) — Label keys used for intelligent grouping embeddings. - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1) - method (string) (required) — Grouping method: 'i' intelligent, 'p' pattern, 'n' none. [i, p, n] @@ -1025,8 +1028,14 @@ Request fields: --is-directly-discard bool — When true, suppressed target alerts are dropped instead of merged. --priority int — Evaluation priority. Lower runs first. --rule-name string (required) — Rule name, 1 to 39 characters. (1-39 chars) - source_filters (array, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. - target_filters (array, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + source_filters (array>, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + - key (string) (required) — Field key (e.g. 'alert_severity', 'labels.service'). + - oper (string) (required) — Filter operator. [IN, NOTIN] + - vals (array) (required) — Values to match. + target_filters (array>, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + - key (string) (required) — Field key (e.g. 'alert_severity', 'labels.service'). + - oper (string) (required) — Filter operator. [IN, NOTIN] + - vals (array) (required) — Values to match. Response fields ('data' envelope is unwrapped — these fields are at the top level): - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID). @@ -1463,7 +1472,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; - group (object) — Alert grouping configuration. - all_equals_required (boolean) — When true, all listed keys must be present for grouping. - cases (array) — Per-filter grouping overrides. - - equals (array) — Groups of label keys whose equality defines a bucket. + - equals (array>) — Groups of label keys whose equality defines a bucket. - i_keys (array) — Label keys used for intelligent grouping embeddings. - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1) - method (string) (required) — Grouping method: 'i' intelligent, 'p' pattern, 'n' none. [i, p, n] @@ -1588,7 +1597,10 @@ Request fields: --is-directly-discard bool — When true, silenced alerts are dropped instead of suppressed into incidents. --priority int — Evaluation priority. Lower runs first. --rule-name string (required) — Rule name, 1 to 39 characters. (1-39 chars) - filters (array, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + filters (array>, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + - key (string) (required) — Field key (e.g. 'alert_severity', 'labels.service'). + - oper (string) (required) — Filter operator. [IN, NOTIN] + - vals (array) (required) — Values to match. time_filter (object, via --data) — One-off time window defined by unix seconds. - end_time (integer) (required) — Window end (unix seconds). - start_time (integer) (required) — Window start (unix seconds). Must be less than 'end_time'. @@ -2012,7 +2024,10 @@ Request fields: --description string — Rule description, up to 500 characters. (≤500 chars) --priority int — Evaluation priority. Lower runs first. --rule-name string (required) — Rule name, 1 to 39 characters. (1-39 chars) - filters (array, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + filters (array>, via --data) — Or-of-and filter tree. Each outer element is an AND group; within each group, all conditions must match. + - key (string) (required) — Field key (e.g. 'alert_severity', 'labels.service'). + - oper (string) (required) — Filter operator. [IN, NOTIN] + - vals (array) (required) — Values to match. Response fields ('data' envelope is unwrapped — these fields are at the top level): - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID). @@ -2403,7 +2418,7 @@ Request fields: group (object, via --data) — Alert grouping configuration. - all_equals_required (boolean) — When true, all listed keys must be present for grouping. - cases (array) — Per-filter grouping overrides. - - equals (array) — Groups of label keys whose equality defines a bucket. + - equals (array>) — Groups of label keys whose equality defines a bucket. - i_keys (array) — Label keys used for intelligent grouping embeddings. - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1) - method (string) (required) — Grouping method: 'i' intelligent, 'p' pattern, 'n' none. [i, p, n] diff --git a/internal/cli/zz_generated_response_help.go b/internal/cli/zz_generated_response_help.go index 0c59024..e01a904 100644 --- a/internal/cli/zz_generated_response_help.go +++ b/internal/cli/zz_generated_response_help.go @@ -43,8 +43,8 @@ var responseHelpBySDKMethod = map[string]string{ "Alerts.ReadInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - account_id (integer) — Account ID.\n - alert_id (string) — Unique alert ID (ObjectID hex string).\n - alert_key (string) — Deduplication key.\n - alert_severity (string) — Current severity. [Critical, Warning, Info, Ok]\n - alert_status (string) — Current status. [Critical, Warning, Info, Ok]\n - channel_id (integer) — ID of the channel the alert belongs to.\n - channel_name (string) — Display name of the channel.\n - channel_status (string) — Status of the channel (e.g. `enabled`, `disabled`).\n - created_at (integer) — Creation timestamp, Unix epoch seconds.\n - data_source_id (integer) — Deprecated. Use `integration_id` instead. Deprecated: use `integration_id` instead.\n - data_source_name (string) — Deprecated. Use `integration_name` instead.\n - data_source_ref_id (string) — Deprecated. Use `integration_ref_id` instead.\n - data_source_type (string) — Deprecated. Use `integration_type` instead.\n - description (string) — Alert description.\n - end_time (integer) — Resolution time, Unix epoch seconds. 0 if still active.\n - event_cnt (integer) — Total number of raw events received by this alert.\n - events (array) — Recent raw events attached to this alert. Populated only by some endpoints.\n - account_id (integer) — Account ID.\n - alert_id (string) — Parent alert ID (MongoDB ObjectID).\n - alert_key (string) — Deduplication key used to merge events into an alert.\n - channel_id (integer) — Channel ID the event is routed to.\n - created_at (integer) — Record creation time, Unix epoch seconds.\n - data_source_id (integer) — Deprecated. Use `integration_id` instead.\n - deleted_at (integer) — Soft-delete timestamp (seconds). Zero if not deleted.\n - description (string) — Event description.\n - event_id (string) — Event ID (MongoDB ObjectID).\n - event_severity (string) — Severity of this event. [Critical, Warning, Info, Ok]\n - event_status (string) — Status of this event. [Critical, Warning, Info, Ok]\n - event_time (integer) — Event timestamp, Unix epoch seconds.\n - images (array) — Images attached to the event.\n - alt (string) — Alt text.\n - href (string) — Optional link URL when the image is clicked.\n - src (string) (required) — Image source URL or internal image reference (starts with `img_` or `http`).\n - integration_id (integer) — Integration that produced this event.\n - integration_type (string) — Type/plugin key of the integration that produced this event.\n - labels (object) — Label key-value pairs.\n - title (string) — Event title.\n - title_rule (string) — Title template used to derive `title` from labels.\n - updated_at (integer) — Record update time, Unix epoch seconds.\n - ever_muted (boolean) — True if this alert has ever been silenced.\n - images (array) — Images attached to the alert.\n - alt (string) — Alt text.\n - href (string) — Optional link URL when the image is clicked.\n - src (string) (required) — Image source URL or internal image reference (starts with `img_` or `http`).\n - incident (object) — Associated incident, if any.\n - incident_id (string) — Incident ID (ObjectID hex string).\n - progress (string) — Incident progress — one of `Triggered`, `Processing`, `Closed`.\n - title (string) — Incident title.\n - integration_id (integer) — ID of the integration that produced this alert.\n - integration_name (string) — Display name of the integration.\n - integration_ref_id (string) — External reference ID of the integration.\n - integration_type (string) — Type/plugin key of the integration.\n - labels (object) — Label key-value pairs.\n - last_time (integer) — Last-event time, Unix epoch seconds.\n - responder_email (string) — Email of the current responder (from the associated incident).\n - responder_name (string) — Display name of the current responder (from the associated incident).\n - start_time (integer) — First-seen time, Unix epoch seconds.\n - title (string) — Alert title.\n - title_rule (string) — Title template used to derive `title` from the event labels (e.g. `$service::$cluster`).\n - updated_at (integer) — Last update timestamp, Unix epoch seconds.\n", "Alerts.ReadList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) — Account ID.\n - alert_id (string) — Unique alert ID (ObjectID hex string).\n - alert_key (string) — Deduplication key.\n - alert_severity (string) — Current severity. [Critical, Warning, Info, Ok]\n - alert_status (string) — Current status. [Critical, Warning, Info, Ok]\n - channel_id (integer) — ID of the channel the alert belongs to.\n - channel_name (string) — Display name of the channel.\n - channel_status (string) — Status of the channel (e.g. `enabled`, `disabled`).\n - created_at (integer) — Creation timestamp, Unix epoch seconds.\n - data_source_id (integer) — Deprecated. Use `integration_id` instead. Deprecated: use `integration_id` instead.\n - data_source_name (string) — Deprecated. Use `integration_name` instead.\n - data_source_ref_id (string) — Deprecated. Use `integration_ref_id` instead.\n - data_source_type (string) — Deprecated. Use `integration_type` instead.\n - description (string) — Alert description.\n - end_time (integer) — Resolution time, Unix epoch seconds. 0 if still active.\n - event_cnt (integer) — Total number of raw events received by this alert.\n - events (array) — Recent raw events attached to this alert. Populated only by some endpoints.\n - account_id (integer) — Account ID.\n - alert_id (string) — Parent alert ID (MongoDB ObjectID).\n - alert_key (string) — Deduplication key used to merge events into an alert.\n - channel_id (integer) — Channel ID the event is routed to.\n - created_at (integer) — Record creation time, Unix epoch seconds.\n - data_source_id (integer) — Deprecated. Use `integration_id` instead.\n - deleted_at (integer) — Soft-delete timestamp (seconds). Zero if not deleted.\n - description (string) — Event description.\n - event_id (string) — Event ID (MongoDB ObjectID).\n - event_severity (string) — Severity of this event. [Critical, Warning, Info, Ok]\n - event_status (string) — Status of this event. [Critical, Warning, Info, Ok]\n - event_time (integer) — Event timestamp, Unix epoch seconds.\n - images (array) — Images attached to the event.\n - alt (string) — Alt text.\n - href (string) — Optional link URL when the image is clicked.\n - src (string) (required) — Image source URL or internal image reference (starts with `img_` or `http`).\n - integration_id (integer) — Integration that produced this event.\n - integration_type (string) — Type/plugin key of the integration that produced this event.\n - labels (object) — Label key-value pairs.\n - title (string) — Event title.\n - title_rule (string) — Title template used to derive `title` from labels.\n - updated_at (integer) — Record update time, Unix epoch seconds.\n - ever_muted (boolean) — True if this alert has ever been silenced.\n - images (array) — Images attached to the alert.\n - alt (string) — Alt text.\n - href (string) — Optional link URL when the image is clicked.\n - src (string) (required) — Image source URL or internal image reference (starts with `img_` or `http`).\n - incident (object) — Associated incident, if any.\n - incident_id (string) — Incident ID (ObjectID hex string).\n - progress (string) — Incident progress — one of `Triggered`, `Processing`, `Closed`.\n - title (string) — Incident title.\n - integration_id (integer) — ID of the integration that produced this alert.\n - integration_name (string) — Display name of the integration.\n - integration_ref_id (string) — External reference ID of the integration.\n - integration_type (string) — Type/plugin key of the integration.\n - labels (object) — Label key-value pairs.\n - last_time (integer) — Last-event time, Unix epoch seconds.\n - responder_email (string) — Email of the current responder (from the associated incident).\n - responder_name (string) — Display name of the current responder (from the associated incident).\n - start_time (integer) — First-seen time, Unix epoch seconds.\n - title (string) — Alert title.\n - title_rule (string) — Title template used to derive `title` from the event labels (e.g. `$service::$cluster`).\n - updated_at (integer) — Last update timestamp, Unix epoch seconds.\n", "Alerts.ReadListByIDs": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) — Account ID.\n - alert_id (string) — Unique alert ID (ObjectID hex string).\n - alert_key (string) — Deduplication key.\n - alert_severity (string) — Current severity. [Critical, Warning, Info, Ok]\n - alert_status (string) — Current status. [Critical, Warning, Info, Ok]\n - channel_id (integer) — ID of the channel the alert belongs to.\n - channel_name (string) — Display name of the channel.\n - channel_status (string) — Status of the channel (e.g. `enabled`, `disabled`).\n - created_at (integer) — Creation timestamp, Unix epoch seconds.\n - data_source_id (integer) — Deprecated. Use `integration_id` instead. Deprecated: use `integration_id` instead.\n - data_source_name (string) — Deprecated. Use `integration_name` instead.\n - data_source_ref_id (string) — Deprecated. Use `integration_ref_id` instead.\n - data_source_type (string) — Deprecated. Use `integration_type` instead.\n - description (string) — Alert description.\n - end_time (integer) — Resolution time, Unix epoch seconds. 0 if still active.\n - event_cnt (integer) — Total number of raw events received by this alert.\n - events (array) — Recent raw events attached to this alert. Populated only by some endpoints.\n - account_id (integer) — Account ID.\n - alert_id (string) — Parent alert ID (MongoDB ObjectID).\n - alert_key (string) — Deduplication key used to merge events into an alert.\n - channel_id (integer) — Channel ID the event is routed to.\n - created_at (integer) — Record creation time, Unix epoch seconds.\n - data_source_id (integer) — Deprecated. Use `integration_id` instead.\n - deleted_at (integer) — Soft-delete timestamp (seconds). Zero if not deleted.\n - description (string) — Event description.\n - event_id (string) — Event ID (MongoDB ObjectID).\n - event_severity (string) — Severity of this event. [Critical, Warning, Info, Ok]\n - event_status (string) — Status of this event. [Critical, Warning, Info, Ok]\n - event_time (integer) — Event timestamp, Unix epoch seconds.\n - images (array) — Images attached to the event.\n - alt (string) — Alt text.\n - href (string) — Optional link URL when the image is clicked.\n - src (string) (required) — Image source URL or internal image reference (starts with `img_` or `http`).\n - integration_id (integer) — Integration that produced this event.\n - integration_type (string) — Type/plugin key of the integration that produced this event.\n - labels (object) — Label key-value pairs.\n - title (string) — Event title.\n - title_rule (string) — Title template used to derive `title` from labels.\n - updated_at (integer) — Record update time, Unix epoch seconds.\n - ever_muted (boolean) — True if this alert has ever been silenced.\n - images (array) — Images attached to the alert.\n - alt (string) — Alt text.\n - href (string) — Optional link URL when the image is clicked.\n - src (string) (required) — Image source URL or internal image reference (starts with `img_` or `http`).\n - incident (object) — Associated incident, if any.\n - incident_id (string) — Incident ID (ObjectID hex string).\n - progress (string) — Incident progress — one of `Triggered`, `Processing`, `Closed`.\n - title (string) — Incident title.\n - integration_id (integer) — ID of the integration that produced this alert.\n - integration_name (string) — Display name of the integration.\n - integration_ref_id (string) — External reference ID of the integration.\n - integration_type (string) — Type/plugin key of the integration.\n - labels (object) — Label key-value pairs.\n - last_time (integer) — Last-event time, Unix epoch seconds.\n - responder_email (string) — Email of the current responder (from the associated incident).\n - responder_name (string) — Display name of the current responder (from the associated incident).\n - start_time (integer) — First-seen time, Unix epoch seconds.\n - title (string) — Alert title.\n - title_rule (string) — Title template used to derive `title` from the event labels (e.g. `$service::$cluster`).\n - updated_at (integer) — Last update timestamp, Unix epoch seconds.\n", - "Alerts.ReadPipelineInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - created_at (integer) — Creation timestamp, Unix epoch seconds.\n - creator_id (integer) — Member ID who created the pipeline.\n - integration_id (integer) — Integration ID this pipeline applies to.\n - rules (array) — Ordered list of processing rules.\n - if (array) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts.\n - kind (string) — Rule type. [title_reset, description_reset, severity_reset, alert_drop, alert_inhibit]\n - settings (object) — Kind-specific settings. Shape depends on `kind`: - `title_reset`: `{ \"title\": \"\" }` - `description_reset`: `{ \"description\": \"\" }` - `severity_reset`: `{ \"severity\": \"Critical\"|\"Warning\"|\"Info\" }` - `alert_drop`: `{}` (empty object) - `alert_inhibit`: `{ \"equals\": [\"\", ...], \"source_filters\": }`\n - description (string) — New description template.\n - equals (array) — Label keys whose values must be equal between the source and current alert for inhibition to apply.\n - severity (string) — Target severity level. [Critical, Warning, Info]\n - source_filters (array) — Filter that identifies the source alerts to inhibit.\n - title (string) — New title template. Supports Golang template syntax referencing alert fields.\n - status (string) — Pipeline status. Possible values: `enabled`, `disabled`.\n - updated_at (integer) — Last update timestamp, Unix epoch seconds.\n - updated_by (integer) — Member ID who last updated the pipeline.\n", - "Alerts.ReadPipelineList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - created_at (integer) — Creation timestamp, Unix epoch seconds.\n - creator_id (integer) — Member ID who created the pipeline.\n - integration_id (integer) — Integration ID this pipeline applies to.\n - rules (array) — Ordered list of processing rules.\n - if (array) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts.\n - kind (string) — Rule type. [title_reset, description_reset, severity_reset, alert_drop, alert_inhibit]\n - settings (object) — Kind-specific settings. Shape depends on `kind`: - `title_reset`: `{ \"title\": \"\" }` - `description_reset`: `{ \"description\": \"\" }` - `severity_reset`: `{ \"severity\": \"Critical\"|\"Warning\"|\"Info\" }` - `alert_drop`: `{}` (empty object) - `alert_inhibit`: `{ \"equals\": [\"\", ...], \"source_filters\": }`\n - description (string) — New description template.\n - equals (array) — Label keys whose values must be equal between the source and current alert for inhibition to apply.\n - severity (string) — Target severity level. [Critical, Warning, Info]\n - source_filters (array) — Filter that identifies the source alerts to inhibit.\n - title (string) — New title template. Supports Golang template syntax referencing alert fields.\n - status (string) — Pipeline status. Possible values: `enabled`, `disabled`.\n - updated_at (integer) — Last update timestamp, Unix epoch seconds.\n - updated_by (integer) — Member ID who last updated the pipeline.\n", + "Alerts.ReadPipelineInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - created_at (integer) — Creation timestamp, Unix epoch seconds.\n - creator_id (integer) — Member ID who created the pipeline.\n - integration_id (integer) — Integration ID this pipeline applies to.\n - rules (array) — Ordered list of processing rules.\n - if (array>) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts.\n - key (string) (required) — Field name to filter on. Use plain names for built-in alert fields (e.g. `alert_severity`, `alert_key`, `check`, `resource`, `service`, `cluster`) or the `labels.` prefix for custom alert labels (e.g. `labels.env`, `labels.region`).\n - oper (string) (required) — Filter operator. `IN` — value must match one of `vals`; `NOTIN` — value must not match any of `vals`. Supports regex patterns wrapped in `/pattern/`. [IN, NOTIN]\n - vals (array) (required) — List of values to match against. Each entry is a plain string or a `/regex/` pattern.\n - kind (string) — Rule type. [title_reset, description_reset, severity_reset, alert_drop, alert_inhibit]\n - settings (object) — Kind-specific settings. Shape depends on `kind`: - `title_reset`: `{ \"title\": \"\" }` - `description_reset`: `{ \"description\": \"\" }` - `severity_reset`: `{ \"severity\": \"Critical\"|\"Warning\"|\"Info\" }` - `alert_drop`: `{}` (empty object) - `alert_inhibit`: `{ \"equals\": [\"\", ...], \"source_filters\": }`\n - description (string) — New description template.\n - equals (array) — Label keys whose values must be equal between the source and current alert for inhibition to apply.\n - severity (string) — Target severity level. [Critical, Warning, Info]\n - source_filters (array>) — Filter that identifies the source alerts to inhibit.\n - key (string) (required) — Field name to filter on. Use plain names for built-in alert fields (e.g. `alert_severity`, `alert_key`, `check`, `resource`, `service`, `cluster`) or the `labels.` prefix for custom alert labels (e.g. `labels.env`, `labels.region`).\n - oper (string) (required) — Filter operator. `IN` — value must match one of `vals`; `NOTIN` — value must not match any of `vals`. Supports regex patterns wrapped in `/pattern/`. [IN, NOTIN]\n - vals (array) (required) — List of values to match against. Each entry is a plain string or a `/regex/` pattern.\n - title (string) — New title template. Supports Golang template syntax referencing alert fields.\n - status (string) — Pipeline status. Possible values: `enabled`, `disabled`.\n - updated_at (integer) — Last update timestamp, Unix epoch seconds.\n - updated_by (integer) — Member ID who last updated the pipeline.\n", + "Alerts.ReadPipelineList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - created_at (integer) — Creation timestamp, Unix epoch seconds.\n - creator_id (integer) — Member ID who created the pipeline.\n - integration_id (integer) — Integration ID this pipeline applies to.\n - rules (array) — Ordered list of processing rules.\n - if (array>) — Optional OR-of-AND filter. When omitted, the rule applies to all alerts.\n - key (string) (required) — Field name to filter on. Use plain names for built-in alert fields (e.g. `alert_severity`, `alert_key`, `check`, `resource`, `service`, `cluster`) or the `labels.` prefix for custom alert labels (e.g. `labels.env`, `labels.region`).\n - oper (string) (required) — Filter operator. `IN` — value must match one of `vals`; `NOTIN` — value must not match any of `vals`. Supports regex patterns wrapped in `/pattern/`. [IN, NOTIN]\n - vals (array) (required) — List of values to match against. Each entry is a plain string or a `/regex/` pattern.\n - kind (string) — Rule type. [title_reset, description_reset, severity_reset, alert_drop, alert_inhibit]\n - settings (object) — Kind-specific settings. Shape depends on `kind`: - `title_reset`: `{ \"title\": \"\" }` - `description_reset`: `{ \"description\": \"\" }` - `severity_reset`: `{ \"severity\": \"Critical\"|\"Warning\"|\"Info\" }` - `alert_drop`: `{}` (empty object) - `alert_inhibit`: `{ \"equals\": [\"\", ...], \"source_filters\": }`\n - description (string) — New description template.\n - equals (array) — Label keys whose values must be equal between the source and current alert for inhibition to apply.\n - severity (string) — Target severity level. [Critical, Warning, Info]\n - source_filters (array>) — Filter that identifies the source alerts to inhibit.\n - title (string) — New title template. Supports Golang template syntax referencing alert fields.\n - status (string) — Pipeline status. Possible values: `enabled`, `disabled`.\n - updated_at (integer) — Last update timestamp, Unix epoch seconds.\n - updated_by (integer) — Member ID who last updated the pipeline.\n", "Analytics.ByAccount": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer)\n - acknowledgement_pct (number)\n - channel_id (integer)\n - channel_name (string)\n - hours (string) — Hour bucket when `split_hours` is enabled. [work, sleep, off]\n - mean_seconds_to_ack (number)\n - mean_seconds_to_close (number)\n - noise_reduction_pct (number)\n - responder_id (integer)\n - responder_name (string)\n - team_id (integer)\n - team_name (string)\n - total_alert_cnt (integer)\n - total_alert_event_cnt (integer)\n - total_engaged_seconds (integer)\n - total_incident_cnt (integer)\n - total_incidents_acknowledged (integer)\n - total_incidents_auto_closed (integer)\n - total_incidents_closed (integer)\n - total_incidents_escalated (integer)\n - total_incidents_manually_closed (integer)\n - total_incidents_manually_escalated (integer)\n - total_incidents_reassigned (integer)\n - total_incidents_timeout_closed (integer)\n - total_incidents_timeout_escalated (integer)\n - total_interruptions (integer)\n - total_notifications (integer)\n - total_seconds_to_ack (integer)\n - total_seconds_to_close (integer)\n - ts (integer) — Aggregation bucket start time, Unix seconds. Present when `aggregate_unit` is used.\n", "Analytics.ByChannel": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer)\n - acknowledgement_pct (number)\n - channel_id (integer)\n - channel_name (string)\n - hours (string) — Hour bucket when `split_hours` is enabled. [work, sleep, off]\n - mean_seconds_to_ack (number)\n - mean_seconds_to_close (number)\n - noise_reduction_pct (number)\n - responder_id (integer)\n - responder_name (string)\n - team_id (integer)\n - team_name (string)\n - total_alert_cnt (integer)\n - total_alert_event_cnt (integer)\n - total_engaged_seconds (integer)\n - total_incident_cnt (integer)\n - total_incidents_acknowledged (integer)\n - total_incidents_auto_closed (integer)\n - total_incidents_closed (integer)\n - total_incidents_escalated (integer)\n - total_incidents_manually_closed (integer)\n - total_incidents_manually_escalated (integer)\n - total_incidents_reassigned (integer)\n - total_incidents_timeout_closed (integer)\n - total_incidents_timeout_escalated (integer)\n - total_interruptions (integer)\n - total_notifications (integer)\n - total_seconds_to_ack (integer)\n - total_seconds_to_close (integer)\n - ts (integer) — Aggregation bucket start time, Unix seconds. Present when `aggregate_unit` is used.\n", "Analytics.ByResponder": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer)\n - acknowledgement_pct (number)\n - channel_id (integer)\n - channel_name (string)\n - hours (string) — Hour bucket when `split_hours` is enabled. [work, sleep, off]\n - mean_seconds_to_ack (number)\n - responder_id (integer)\n - responder_name (string)\n - team_id (integer)\n - team_name (string)\n - total_engaged_seconds (integer)\n - total_incident_cnt (integer)\n - total_incidents_acknowledged (integer)\n - total_incidents_escalated (integer)\n - total_incidents_manually_escalated (integer)\n - total_incidents_reassigned (integer)\n - total_incidents_timeout_escalated (integer)\n - total_interruptions (integer)\n - total_notifications (integer)\n - total_seconds_to_ack (integer)\n - ts (integer) — Aggregation bucket start time, Unix seconds. Present when `aggregate_unit` is used.\n", @@ -75,11 +75,11 @@ var responseHelpBySDKMethod = map[string]string{ "Channels.ChannelEscalateRuleCreate": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name echoed back from the request.\n", "Channels.ChannelEscalateRuleInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - account_id (integer) (required) — Owning account ID.\n - aggr_window (integer) (required) — Delay window in seconds.\n - channel_id (integer) (required) — Channel the rule belongs to.\n - channel_name (string) — Channel name, populated for cross-channel listing responses.\n - created_at (integer) (required) — Creation timestamp (unix seconds).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Emitted only for soft-deleted rules.\n - description (string) (required) — Rule description.\n - filters (object) (required)\n - layers (array) (required) — Escalation levels in order.\n - escalate_window (integer) — Wait before moving to the next level, in minutes. (0-720)\n - force_escalate (boolean) — When true, always escalate regardless of acknowledgement.\n - max_times (integer) — Max repeat notifications within the level. (0-6)\n - notify_step (number) — Repeat interval in minutes. (0.5-120)\n - target (object) (required) — Notification target. At least one of `person_ids`, `team_ids`, `schedule_to_role_ids`, or `emails` must be set, together with either `by` or `webhooks`.\n - by (object) — Per-severity personal notification channels. Required unless `webhooks` is provided.\n - critical (array) — Channels for Critical events (e.g. `voice`, `sms`, `email`, `feishu`).\n - follow_preference (boolean) — When true, use each responder's personal preference instead of the lists below.\n - info (array) — Channels for Info events.\n - warning (array) — Channels for Warning events.\n - emails (array) — Email addresses to notify (push-only scenarios).\n - person_ids (array) — Member IDs to notify directly.\n - schedule_to_role_ids (object) — Map of schedule ID to the role IDs on that schedule to notify.\n - team_ids (array) — Team IDs to notify.\n - webhooks (array) — Group chat / webhook targets. Required unless `by` is provided.\n - settings (object) (required) — Type-specific settings (chat IDs, URLs, etc.).\n - type (string) (required) — Webhook type (e.g. `feishu`, `dingtalk_app`, `wecom_app`, `slack`, `teams`, `custom`).\n - priority (integer) (required) — Evaluation priority. Lower runs first.\n - rule_id (string) (required) — Escalation rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name.\n - status (string) (required) — Rule status. [enabled, disabled]\n - template_id (string) (required) — Notification template ID (MongoDB ObjectID).\n - time_filters (array) (required) — Recurring time windows during which the rule applies.\n - cal_id (string) — Optional calendar ID; restricts the window to days matching the calendar.\n - end (string) — End of the window in `HH:MM`.\n - is_off (boolean) — When true, match days marked as days-off in the calendar.\n - repeat (array) — Days of the week this window repeats on. Empty means every day.\n - start (string) — Start of the window in `HH:MM`.\n - updated_at (integer) (required) — Last update timestamp (unix seconds).\n - updated_by (integer) (required) — Member ID that last updated the rule.\n", "Channels.ChannelEscalateRuleList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) (required) — Owning account ID.\n - aggr_window (integer) (required) — Delay window in seconds.\n - channel_id (integer) (required) — Channel the rule belongs to.\n - channel_name (string) — Channel name, populated for cross-channel listing responses.\n - created_at (integer) (required) — Creation timestamp (unix seconds).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Emitted only for soft-deleted rules.\n - description (string) (required) — Rule description.\n - filters (object) (required)\n - layers (array) (required) — Escalation levels in order.\n - escalate_window (integer) — Wait before moving to the next level, in minutes. (0-720)\n - force_escalate (boolean) — When true, always escalate regardless of acknowledgement.\n - max_times (integer) — Max repeat notifications within the level. (0-6)\n - notify_step (number) — Repeat interval in minutes. (0.5-120)\n - target (object) (required) — Notification target. At least one of `person_ids`, `team_ids`, `schedule_to_role_ids`, or `emails` must be set, together with either `by` or `webhooks`.\n - by (object) — Per-severity personal notification channels. Required unless `webhooks` is provided.\n - emails (array) — Email addresses to notify (push-only scenarios).\n - person_ids (array) — Member IDs to notify directly.\n - schedule_to_role_ids (object) — Map of schedule ID to the role IDs on that schedule to notify.\n - team_ids (array) — Team IDs to notify.\n - webhooks (array) — Group chat / webhook targets. Required unless `by` is provided.\n - priority (integer) (required) — Evaluation priority. Lower runs first.\n - rule_id (string) (required) — Escalation rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name.\n - status (string) (required) — Rule status. [enabled, disabled]\n - template_id (string) (required) — Notification template ID (MongoDB ObjectID).\n - time_filters (array) (required) — Recurring time windows during which the rule applies.\n - cal_id (string) — Optional calendar ID; restricts the window to days matching the calendar.\n - end (string) — End of the window in `HH:MM`.\n - is_off (boolean) — When true, match days marked as days-off in the calendar.\n - repeat (array) — Days of the week this window repeats on. Empty means every day.\n - start (string) — Start of the window in `HH:MM`.\n - updated_at (integer) (required) — Last update timestamp (unix seconds).\n - updated_by (integer) (required) — Member ID that last updated the rule.\n", - "Channels.ChannelInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - account_id (integer) — Owning account ID.\n - active_incident_highest_severity (string) — Highest severity among active incidents in the channel.\n - auto_resolve_mode (string) — Auto-resolve timer reset mode. [trigger, update]\n - auto_resolve_timeout (integer) — Auto-resolve timeout in seconds. 0 disables auto-resolve.\n - channel_id (integer) — Channel ID.\n - channel_name (string) — Channel name.\n - created_at (integer) — Creation timestamp (unix seconds).\n - creator_id (integer) — Member ID who created the channel.\n - creator_name (string) — Name of the member who created the channel (resolved from the member directory; empty when unavailable).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels.\n - description (string) — Free-form description.\n - disable_auto_close (boolean) — When true, automatic incident closing is disabled.\n - disable_outlier_detection (boolean) — When true, outlier incident detection is disabled.\n - external_report_token (string) — Token granted to external reporters when external reporting is enabled.\n - flapping (object) — Flapping detection configuration.\n - in_mins (integer) — Observation window in minutes. (1-1440)\n - is_disabled (boolean) — Disable flapping detection.\n - max_changes (integer) — Max state changes allowed within `in_mins`. (2-100)\n - mute_mins (integer) — Mute duration in minutes after flapping is detected. (0-1440)\n - group (object) — Alert grouping configuration.\n - all_equals_required (boolean) — When true, all listed keys must be present for grouping.\n - cases (array) — Per-filter grouping overrides.\n - equals (array) — Groups of label keys whose equality defines a bucket.\n - i_keys (array) — Label keys used for intelligent grouping embeddings.\n - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1)\n - method (string) (required) — Grouping method: `i` intelligent, `p` pattern, `n` none. [i, p, n]\n - storm_threshold (integer) — Alert storm threshold. (0-10000)\n - storm_thresholds (array) — Multi-level storm thresholds.\n - time_window (integer) — Grouping time window in minutes. Default max is 1440 minutes (24 h); extended accounts may allow up to 43200 minutes (30 days). (min 0)\n - window_type (string) — Window type. Defaults to `tumbling`. [tumbling, sliding]\n - is_external_report_enabled (boolean) — Whether external reporters can file incidents into this channel.\n - is_private (boolean) — When true, the channel is visible only to its managing teams.\n - is_starred (boolean) — Whether the current user has starred this channel.\n - last_incident_at (integer) — Timestamp of the most recent incident (unix seconds).\n - managing_team_ids (array) — Additional teams that can manage the channel.\n - progress_to_incident_cnts (object)\n - Processing (integer) (required) — Count of processing incidents in the last 30 days.\n - Triggered (integer) (required) — Count of triggered incidents in the last 30 days.\n - status (string) — Channel status. [enabled, disabled, deleted]\n - team_id (integer) — Owning team ID.\n - team_name (string) — Owning team name (resolved from the team directory; empty when unavailable).\n - updated_at (integer) — Last update timestamp (unix seconds).\n", + "Channels.ChannelInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - account_id (integer) — Owning account ID.\n - active_incident_highest_severity (string) — Highest severity among active incidents in the channel.\n - auto_resolve_mode (string) — Auto-resolve timer reset mode. [trigger, update]\n - auto_resolve_timeout (integer) — Auto-resolve timeout in seconds. 0 disables auto-resolve.\n - channel_id (integer) — Channel ID.\n - channel_name (string) — Channel name.\n - created_at (integer) — Creation timestamp (unix seconds).\n - creator_id (integer) — Member ID who created the channel.\n - creator_name (string) — Name of the member who created the channel (resolved from the member directory; empty when unavailable).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels.\n - description (string) — Free-form description.\n - disable_auto_close (boolean) — When true, automatic incident closing is disabled.\n - disable_outlier_detection (boolean) — When true, outlier incident detection is disabled.\n - external_report_token (string) — Token granted to external reporters when external reporting is enabled.\n - flapping (object) — Flapping detection configuration.\n - in_mins (integer) — Observation window in minutes. (1-1440)\n - is_disabled (boolean) — Disable flapping detection.\n - max_changes (integer) — Max state changes allowed within `in_mins`. (2-100)\n - mute_mins (integer) — Mute duration in minutes after flapping is detected. (0-1440)\n - group (object) — Alert grouping configuration.\n - all_equals_required (boolean) — When true, all listed keys must be present for grouping.\n - cases (array) — Per-filter grouping overrides.\n - equals (array>) — Groups of label keys whose equality defines a bucket.\n - i_keys (array) — Label keys used for intelligent grouping embeddings.\n - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1)\n - method (string) (required) — Grouping method: `i` intelligent, `p` pattern, `n` none. [i, p, n]\n - storm_threshold (integer) — Alert storm threshold. (0-10000)\n - storm_thresholds (array) — Multi-level storm thresholds.\n - time_window (integer) — Grouping time window in minutes. Default max is 1440 minutes (24 h); extended accounts may allow up to 43200 minutes (30 days). (min 0)\n - window_type (string) — Window type. Defaults to `tumbling`. [tumbling, sliding]\n - is_external_report_enabled (boolean) — Whether external reporters can file incidents into this channel.\n - is_private (boolean) — When true, the channel is visible only to its managing teams.\n - is_starred (boolean) — Whether the current user has starred this channel.\n - last_incident_at (integer) — Timestamp of the most recent incident (unix seconds).\n - managing_team_ids (array) — Additional teams that can manage the channel.\n - progress_to_incident_cnts (object)\n - Processing (integer) (required) — Count of processing incidents in the last 30 days.\n - Triggered (integer) (required) — Count of triggered incidents in the last 30 days.\n - status (string) — Channel status. [enabled, disabled, deleted]\n - team_id (integer) — Owning team ID.\n - team_name (string) — Owning team name (resolved from the team directory; empty when unavailable).\n - updated_at (integer) — Last update timestamp (unix seconds).\n", "Channels.ChannelInfos": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - channel_id (integer) (required) — Channel ID.\n - channel_name (string) (required) — Channel name.\n - status (string) — Channel status. [enabled, disabled]\n", "Channels.ChannelInhibitRuleCreate": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name echoed back from the request.\n", "Channels.ChannelInhibitRuleList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) (required)\n - channel_id (integer) (required)\n - created_at (integer) (required)\n - deleted_at (integer)\n - description (string) (required)\n - equals (array) (required) — Label keys used to pair source and target alerts.\n - is_directly_discard (boolean) (required)\n - priority (integer) (required)\n - rule_id (string) (required)\n - rule_name (string) (required)\n - source_filters (object) (required)\n - status (string) (required) [enabled, disabled]\n - target_filters (object) (required)\n - updated_at (integer) (required)\n - updated_by (integer) (required)\n", - "Channels.ChannelList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) — Owning account ID.\n - active_incident_highest_severity (string) — Highest severity among active incidents in the channel.\n - auto_resolve_mode (string) — Auto-resolve timer reset mode. [trigger, update]\n - auto_resolve_timeout (integer) — Auto-resolve timeout in seconds. 0 disables auto-resolve.\n - channel_id (integer) — Channel ID.\n - channel_name (string) — Channel name.\n - created_at (integer) — Creation timestamp (unix seconds).\n - creator_id (integer) — Member ID who created the channel.\n - creator_name (string) — Name of the member who created the channel (resolved from the member directory; empty when unavailable).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels.\n - description (string) — Free-form description.\n - disable_auto_close (boolean) — When true, automatic incident closing is disabled.\n - disable_outlier_detection (boolean) — When true, outlier incident detection is disabled.\n - external_report_token (string) — Token granted to external reporters when external reporting is enabled.\n - flapping (object) — Flapping detection configuration.\n - in_mins (integer) — Observation window in minutes. (1-1440)\n - is_disabled (boolean) — Disable flapping detection.\n - max_changes (integer) — Max state changes allowed within `in_mins`. (2-100)\n - mute_mins (integer) — Mute duration in minutes after flapping is detected. (0-1440)\n - group (object) — Alert grouping configuration.\n - all_equals_required (boolean) — When true, all listed keys must be present for grouping.\n - cases (array) — Per-filter grouping overrides.\n - equals (array) — Groups of label keys whose equality defines a bucket.\n - i_keys (array) — Label keys used for intelligent grouping embeddings.\n - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1)\n - method (string) (required) — Grouping method: `i` intelligent, `p` pattern, `n` none. [i, p, n]\n - storm_threshold (integer) — Alert storm threshold. (0-10000)\n - storm_thresholds (array) — Multi-level storm thresholds.\n - time_window (integer) — Grouping time window in minutes. Default max is 1440 minutes (24 h); extended accounts may allow up to 43200 minutes (30 days). (min 0)\n - window_type (string) — Window type. Defaults to `tumbling`. [tumbling, sliding]\n - is_external_report_enabled (boolean) — Whether external reporters can file incidents into this channel.\n - is_private (boolean) — When true, the channel is visible only to its managing teams.\n - is_starred (boolean) — Whether the current user has starred this channel.\n - last_incident_at (integer) — Timestamp of the most recent incident (unix seconds).\n - managing_team_ids (array) — Additional teams that can manage the channel.\n - progress_to_incident_cnts (object)\n - Processing (integer) (required) — Count of processing incidents in the last 30 days.\n - Triggered (integer) (required) — Count of triggered incidents in the last 30 days.\n - status (string) — Channel status. [enabled, disabled, deleted]\n - team_id (integer) — Owning team ID.\n - team_name (string) — Owning team name (resolved from the team directory; empty when unavailable).\n - updated_at (integer) — Last update timestamp (unix seconds).\n", + "Channels.ChannelList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) — Owning account ID.\n - active_incident_highest_severity (string) — Highest severity among active incidents in the channel.\n - auto_resolve_mode (string) — Auto-resolve timer reset mode. [trigger, update]\n - auto_resolve_timeout (integer) — Auto-resolve timeout in seconds. 0 disables auto-resolve.\n - channel_id (integer) — Channel ID.\n - channel_name (string) — Channel name.\n - created_at (integer) — Creation timestamp (unix seconds).\n - creator_id (integer) — Member ID who created the channel.\n - creator_name (string) — Name of the member who created the channel (resolved from the member directory; empty when unavailable).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels.\n - description (string) — Free-form description.\n - disable_auto_close (boolean) — When true, automatic incident closing is disabled.\n - disable_outlier_detection (boolean) — When true, outlier incident detection is disabled.\n - external_report_token (string) — Token granted to external reporters when external reporting is enabled.\n - flapping (object) — Flapping detection configuration.\n - in_mins (integer) — Observation window in minutes. (1-1440)\n - is_disabled (boolean) — Disable flapping detection.\n - max_changes (integer) — Max state changes allowed within `in_mins`. (2-100)\n - mute_mins (integer) — Mute duration in minutes after flapping is detected. (0-1440)\n - group (object) — Alert grouping configuration.\n - all_equals_required (boolean) — When true, all listed keys must be present for grouping.\n - cases (array) — Per-filter grouping overrides.\n - equals (array>) — Groups of label keys whose equality defines a bucket.\n - i_keys (array) — Label keys used for intelligent grouping embeddings.\n - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1)\n - method (string) (required) — Grouping method: `i` intelligent, `p` pattern, `n` none. [i, p, n]\n - storm_threshold (integer) — Alert storm threshold. (0-10000)\n - storm_thresholds (array) — Multi-level storm thresholds.\n - time_window (integer) — Grouping time window in minutes. Default max is 1440 minutes (24 h); extended accounts may allow up to 43200 minutes (30 days). (min 0)\n - window_type (string) — Window type. Defaults to `tumbling`. [tumbling, sliding]\n - is_external_report_enabled (boolean) — Whether external reporters can file incidents into this channel.\n - is_private (boolean) — When true, the channel is visible only to its managing teams.\n - is_starred (boolean) — Whether the current user has starred this channel.\n - last_incident_at (integer) — Timestamp of the most recent incident (unix seconds).\n - managing_team_ids (array) — Additional teams that can manage the channel.\n - progress_to_incident_cnts (object)\n - Processing (integer) (required) — Count of processing incidents in the last 30 days.\n - Triggered (integer) (required) — Count of triggered incidents in the last 30 days.\n - status (string) — Channel status. [enabled, disabled, deleted]\n - team_id (integer) — Owning team ID.\n - team_name (string) — Owning team name (resolved from the team directory; empty when unavailable).\n - updated_at (integer) — Last update timestamp (unix seconds).\n", "Channels.ChannelSilenceRuleCreate": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name echoed back from the request.\n", "Channels.ChannelSilenceRuleList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) (required)\n - channel_id (integer) (required)\n - created_at (integer) (required)\n - deleted_at (integer)\n - description (string) (required)\n - filters (object) (required)\n - from_incident_id (string) — Source incident ID when the silence was created from an incident.\n - is_auto_delete (boolean) — When true, the silence rule is automatically deleted after its time window expires. Defaults to false.\n - is_directly_discard (boolean) (required) — When true, silenced alerts are dropped instead of suppressed into incidents.\n - is_effective (boolean) (required) — Whether the rule is currently in effect.\n - priority (integer) (required) — Evaluation priority. Lower runs first.\n - rule_id (string) (required)\n - rule_name (string) (required)\n - status (string) (required) [enabled, disabled]\n - time_filter (object) (required) — One-off time window defined by unix seconds.\n - end_time (integer) (required) — Window end (unix seconds). Must be > 0.\n - start_time (integer) (required) — Window start (unix seconds). Must be > 0 and less than `end_time`.\n - time_filters (array) (required) — Recurring time windows.\n - cal_id (string) — Optional calendar ID; restricts the window to days matching the calendar.\n - end (string) — End of the window in `HH:MM`.\n - is_off (boolean) — When true, match days marked as days-off in the calendar.\n - repeat (array) — Days of the week this window repeats on. Empty means every day.\n - start (string) — Start of the window in `HH:MM`.\n - updated_at (integer) (required)\n - updated_by (integer) (required)\n", "Channels.ChannelUnsubscribeRuleCreate": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name echoed back from the request.\n", diff --git a/internal/cmd/cligen/main.go b/internal/cmd/cligen/main.go index c0fc478..6713b8d 100644 --- a/internal/cmd/cligen/main.go +++ b/internal/cmd/cligen/main.go @@ -481,19 +481,14 @@ func (w *specWalker) enumOf(s map[string]any) []string { return nil } -// schemaType renders a compact type label for a (deref'd) property schema. +// schemaType renders a compact type label for a (deref'd) property schema, +// recursing through nested array `items` so an "Or-of-AND" filter tree +// (array>) renders its true element type instead of collapsing +// to the uninformative "array" one level down. func schemaType(s map[string]any) string { switch t := str(s, "type"); t { case "array": - it := asMap(s["items"]) - et := str(it, "type") - if et == "" && (it["properties"] != nil || it["allOf"] != nil || it["$ref"] != nil) { - et = "object" - } - if et == "" { - et = "any" - } - return "array<" + et + ">" + return "array<" + schemaType(asMap(s["items"])) + ">" case "": if s["properties"] != nil || s["allOf"] != nil || s["$ref"] != nil { return "object" @@ -552,8 +547,24 @@ func numStr(v any) (string, bool) { // maxSchemaDepth bounds how deep request/response trees are expanded in --help. const maxSchemaDepth = 3 +// arrayLeafSchema unwraps an array schema's `items` (deref'ing $ref at each +// level) through any number of nested array layers — e.g. the "Or-of-AND" +// filter trees (`array>`) — until it reaches the first +// non-array element schema. A plain `array` schema returns its object +// items unchanged (zero unwraps), so tree()'s array case handles any nesting +// depth the same way it always handled one level. Bounded by maxSchemaDepth as +// a defensive stop against a cyclic schema. +func (w *specWalker) arrayLeafSchema(s map[string]any) map[string]any { + it := w.deref(asMap(s["items"])) + for levels := 0; str(it, "type") == "array" && levels < maxSchemaDepth; levels++ { + it = w.deref(asMap(it["items"])) + } + return it +} + // tree walks an object schema (resolving $ref/allOf) into a sorted field tree, -// recursing into nested objects and array-element objects up to maxSchemaDepth. +// recursing into nested objects and array-element objects (through any depth +// of nested arrays, see arrayLeafSchema) up to maxSchemaDepth. func (w *specWalker) tree(schema map[string]any, depth int) []schemaField { if depth > maxSchemaDepth { return nil @@ -576,11 +587,11 @@ func (w *specWalker) tree(schema map[string]any, depth int) []schemaField { f.Type = "object" f.Children = w.tree(pv, depth+1) case str(pv, "type") == "array": - it := w.deref(asMap(pv["items"])) + it := w.arrayLeafSchema(pv) if w.isObjectSchema(it) { f.Children = w.tree(it, depth+1) } else if len(f.Enum) == 0 { - f.Enum = enumStrings(it) // array of constrained scalars + f.Enum = enumStrings(it) // array (possibly nested) of constrained scalars } } out = append(out, f) diff --git a/internal/cmd/cligen/nested_array_test.go b/internal/cmd/cligen/nested_array_test.go new file mode 100644 index 0000000..c43aeb0 --- /dev/null +++ b/internal/cmd/cligen/nested_array_test.go @@ -0,0 +1,114 @@ +package main + +import "testing" + +// TestTreeExpandsNestedArrayOfObject covers the "Or-of-AND" filter tree shape +// (array>, e.g. silence-rule-create's `filters`): the object +// item fields two array layers down must render as children exactly like a +// plain array field does one layer down. +func TestTreeExpandsNestedArrayOfObject(t *testing.T) { + w := &specWalker{schemas: map[string]any{ + "FilterCondition": map[string]any{ + "type": "object", + "required": []any{"key", "oper", "vals"}, + "properties": map[string]any{ + "key": map[string]any{"type": "string", "description": "e.g. `alert_severity`, `labels.service`"}, + "oper": map[string]any{"type": "string", "enum": []any{"IN", "NOTIN"}}, + "vals": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, + }, + }, + }} + + fields := w.tree(map[string]any{"properties": map[string]any{ + "filters": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "array", + "items": map[string]any{"$ref": "#/components/schemas/FilterCondition"}, + }, + }, + }}, 0) + + if len(fields) != 1 || fields[0].Wire != "filters" { + t.Fatalf("fields = %#v, want single 'filters' field", fields) + } + children := fields[0].Children + if len(children) != 3 { + t.Fatalf("filters.Children = %#v, want 3 (key, oper, vals)", children) + } + byWire := map[string]schemaField{} + for _, c := range children { + byWire[c.Wire] = c + } + if byWire["key"].Type != "string" || byWire["key"].Desc == "" { + t.Fatalf("key child = %#v, want string with description", byWire["key"]) + } + if len(byWire["oper"].Enum) != 2 { + t.Fatalf("oper.Enum = %#v, want [IN NOTIN]", byWire["oper"].Enum) + } + if byWire["vals"].Type != "array" { + t.Fatalf("vals.Type = %q, want array", byWire["vals"].Type) + } +} + +// TestTreeLeavesNestedArrayOfScalarWithoutChildren guards the sibling shape +// (array>, e.g. the alert-grouping `equals` field): with no +// object at the bottom, tree() must not synthesize children. +func TestTreeLeavesNestedArrayOfScalarWithoutChildren(t *testing.T) { + w := &specWalker{} + + fields := w.tree(map[string]any{"properties": map[string]any{ + "equals": map[string]any{ + "type": "array", + "items": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, + }, + }}, 0) + + if len(fields) != 1 || fields[0].Wire != "equals" { + t.Fatalf("fields = %#v, want single 'equals' field", fields) + } + if len(fields[0].Children) != 0 { + t.Fatalf("equals.Children = %#v, want none", fields[0].Children) + } +} + +// TestSchemaTypeLabelsNestedArrayDepth guards the type label alongside the +// child-expansion behavior above: "array" hid the same missing-depth +// information for the flag/summary line that the missing children hid for +// the field list, and both stem from the array case only unwrapping one +// `items` level. +func TestSchemaTypeLabelsNestedArrayDepth(t *testing.T) { + cases := []struct { + name string + s map[string]any + want string + }{ + {"scalar array", map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, "array"}, + {"array of object", map[string]any{"type": "array", "items": map[string]any{"type": "object"}}, "array"}, + {"array of array of object (filters)", map[string]any{"type": "array", "items": map[string]any{"type": "array", "items": map[string]any{"$ref": "#/components/schemas/FilterCondition"}}}, "array>"}, + {"array of array of string (equals)", map[string]any{"type": "array", "items": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}}, "array>"}, + } + for _, c := range cases { + if got := schemaType(c.s); got != c.want { + t.Errorf("%s: schemaType() = %q, want %q", c.name, got, c.want) + } + } +} + +// TestTreeExpandsPlainArrayOfObjectUnchanged is a regression guard: the +// existing single-level array behavior (e.g. `layers`) must be +// unaffected by generalizing the array case to unwrap nested arrays. +func TestTreeExpandsPlainArrayOfObjectUnchanged(t *testing.T) { + w := &specWalker{} + + fields := w.tree(map[string]any{"properties": map[string]any{ + "layers": map[string]any{ + "type": "array", + "items": map[string]any{"type": "object", "properties": map[string]any{"target": map[string]any{"type": "string"}}}, + }, + }}, 0) + + if len(fields) != 1 || len(fields[0].Children) != 1 || fields[0].Children[0].Wire != "target" { + t.Fatalf("layers field = %#v, want single child 'target'", fields[0]) + } +} diff --git a/internal/cmd/skilldoc/main.go b/internal/cmd/skilldoc/main.go index 16e57a2..0cef477 100644 --- a/internal/cmd/skilldoc/main.go +++ b/internal/cmd/skilldoc/main.go @@ -39,7 +39,7 @@ func main() { func genCmd() *cobra.Command { return &cobra.Command{ Use: "gen [group]", - Short: "Rewrite the generated fence in skills/flashduty/reference/.md (every card if no group given)", + Short: "Rewrite every GENERATED: fence across the skills/flashduty cards (every group if none given)", Args: cobra.MaximumNArgs(1), RunE: func(_ *cobra.Command, args []string) error { base, err := cardBase() @@ -81,40 +81,91 @@ func checkCmd() *cobra.Command { // dump builds the command-tree dump from the live CLI root, in-process. func dump() skilldoc.Dump { return skilldoc.Build(cli.RootForDump()) } -// runGen rewrites the GENERATED: fence inside /reference/.md -// with a fresh render, leaving all hand-written content outside the fence -// untouched. -func runGen(d skilldoc.Dump, base, group string) error { - card := filepath.Join(base, "reference", group+".md") - raw, err := os.ReadFile(card) - if err != nil { - return fmt.Errorf("read card: %w", err) +// genGroup regenerates every GENERATED fence of group across the already- +// loaded docs, leaving hand-written content outside the fences untouched. A +// group may split its fences across cards (subset fences claiming verb +// prefixes, plus the catch-all for the rest — see skilldoc.RenderGroupFences), +// so the fresh render is computed for the group as a whole, then spliced per +// card. Rewritten bodies are written to disk AND updated in docs, so a caller +// looping over groups keeps seeing current content. found is false when no +// fence of the group exists anywhere. +func genGroup(d skilldoc.Dump, base string, docs []skilldoc.Doc, group string) (found bool, err error) { + var ids []string + perDoc := map[string][]string{} + for _, doc := range docs { + for _, fl := range skilldoc.FenceLocs(doc.Body) { + spec, err := skilldoc.ParseFenceID(fl.ID) + if err != nil { + return false, fmt.Errorf("%s: %w", doc.Path, err) + } + if spec.Group != group { + continue + } + perDoc[doc.Path] = append(perDoc[doc.Path], fl.ID) + ids = append(ids, fl.ID) + } + } + if len(ids) == 0 { + return false, nil } - body := normalizeEOL(string(raw)) - start, end := skilldoc.FenceStart(group), skilldoc.FenceEnd(group) - si := strings.Index(body, start) - ei := strings.Index(body, end) - if si < 0 || ei < 0 || ei < si { - return fmt.Errorf("%s: no GENERATED:%s fence to fill (add the start/end markers first)", card, group) + rendered, violations := skilldoc.RenderGroupFences(d, group, ids) + if len(violations) > 0 { + return true, fmt.Errorf("group %s fence topology: %s", group, strings.Join(violations, "; ")) } - fresh := skilldoc.GenerateFence(d, group) - updated := body[:si] + fresh + body[ei+len(end):] - if updated == body { - return nil // already fresh + for i, doc := range docs { + docIDs := perDoc[doc.Path] + if len(docIDs) == 0 { + continue + } + body := doc.Body + for _, id := range docIDs { + start, end, ok := skilldoc.FindFence(body, id) + if !ok { + return true, fmt.Errorf("%s: unterminated GENERATED:%s fence", doc.Path, id) + } + body = body[:start] + rendered[id] + body[end:] + } + if body == doc.Body { + continue // already fresh + } + if err := os.WriteFile(filepath.Join(base, doc.Path), []byte(body), 0o644); err != nil { + return true, fmt.Errorf("write card: %w", err) + } + docs[i].Body = body } - if err := os.WriteFile(card, []byte(updated), 0o644); err != nil { - return fmt.Errorf("write card: %w", err) + return true, nil +} + +// runGen regenerates one group's fences. +func runGen(d skilldoc.Dump, base, group string) error { + docs, err := loadDocs(base) + if err != nil { + return err + } + found, err := genGroup(d, base, docs, group) + if err != nil { + return err + } + if !found { + return fmt.Errorf("no GENERATED:%s fence found under %s (add the start/end markers first)", group, base) } return nil } -// runGenAll regenerates the fence of every dump group that has a card file under -// /reference. The group set is derived from the dump (intersected with the -// cards that actually exist), so it stays correct as domains are added or -// renamed — no hardcoded list. Groups without a card (e.g. webhook) are skipped. +// runGenAll regenerates the fences of every dump group that has at least one +// GENERATED marker in a card under . The group set is derived from the +// dump (intersected with the fences that actually exist, which genGroup +// reports via found), so it stays correct as domains are added or renamed — +// no hardcoded list. Groups without any fence (e.g. webhook) are skipped. +// The corpus is loaded once and threaded through every group. func runGenAll(d skilldoc.Dump, base string) error { + docs, err := loadDocs(base) + if err != nil { + return err + } + seen := map[string]bool{} var groups []string for _, c := range d.Commands { @@ -125,10 +176,7 @@ func runGenAll(d skilldoc.Dump, base string) error { } sort.Strings(groups) for _, g := range groups { - if _, err := os.Stat(filepath.Join(base, "reference", g+".md")); err != nil { - continue // no card for this group - } - if err := runGen(d, base, g); err != nil { + if _, err := genGroup(d, base, docs, g); err != nil { return fmt.Errorf("gen %s: %w", g, err) } } diff --git a/internal/cmd/skilldoc/main_test.go b/internal/cmd/skilldoc/main_test.go index 758b59c..922b7c9 100644 --- a/internal/cmd/skilldoc/main_test.go +++ b/internal/cmd/skilldoc/main_test.go @@ -337,3 +337,105 @@ func TestRunGen_FillsFence(t *testing.T) { t.Errorf("gen clobbered hand-written content:\n%s", updated) } } + +// TestRunGen_SplitAcrossCards is the split-card path: one group whose subset +// fence and catch-all fence live in different files. gen must fill both from +// one group-wide render, and check must then be clean. +func TestRunGen_SplitAcrossCards(t *testing.T) { + dir := t.TempDir() + mk := func(verb string) skilldoc.Command { + return skilldoc.Command{Path: "svc " + verb, Group: "svc", Short: "S " + verb, Use: verb} + } + d := skilldoc.Dump{Commands: []skilldoc.Command{mk("list"), mk("rule-create"), mk("rule-delete")}} + + rules := filepath.Join(dir, "reference", "rules.md") + svc := filepath.Join(dir, "reference", "svc.md") + writeFile(t, rules, "# rules\n\n"+skilldoc.FenceStart("svc[rule]")+"\n"+skilldoc.FenceEnd("svc[rule]")+"\n") + writeFile(t, svc, "# svc\n\nintro\n\n"+skilldoc.FenceStart("svc")+"\n"+skilldoc.FenceEnd("svc")+"\n") + + if err := runGen(d, dir, "svc"); err != nil { + t.Fatalf("runGen: %v", err) + } + + rulesBody, err := os.ReadFile(rules) + if err != nil { + t.Fatal(err) + } + svcBody, err := os.ReadFile(svc) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(rulesBody), "### rule-create") || strings.Contains(string(rulesBody), "### list") { + t.Errorf("rules card should carry exactly the claimed verbs:\n%s", rulesBody) + } + if !strings.Contains(string(svcBody), "### list") || strings.Contains(string(svcBody), "### rule-create") { + t.Errorf("svc card should carry exactly the unclaimed remainder:\n%s", svcBody) + } + if !strings.Contains(string(svcBody), "intro") { + t.Errorf("gen clobbered hand-written content:\n%s", svcBody) + } + + var out bytes.Buffer + if n, _ := runCheck(d, dir, &out); n != 0 { + t.Errorf("after gen, check should be clean; got %d:\n%s", n, out.String()) + } +} + +// TestRunGen_TopologyViolationFails asserts gen refuses to write anything when +// the group's fences do not partition its verbs. +func TestRunGen_TopologyViolationFails(t *testing.T) { + dir := t.TempDir() + mk := func(verb string) skilldoc.Command { + return skilldoc.Command{Path: "svc " + verb, Group: "svc", Short: "S " + verb, Use: verb} + } + d := skilldoc.Dump{Commands: []skilldoc.Command{mk("list"), mk("rule-create")}} + + // Subset fence only — "list" has no home. + writeFile(t, filepath.Join(dir, "reference", "rules.md"), + "# rules\n\n"+skilldoc.FenceStart("svc[rule]")+"\n"+skilldoc.FenceEnd("svc[rule]")+"\n") + + err := runGen(d, dir, "svc") + if err == nil || !strings.Contains(err.Error(), "no catch-all") { + t.Fatalf("want topology error mentioning the missing catch-all, got %v", err) + } +} + +// TestRunGen_TwoFencesInOneFile pins the sequential splice loop: a single card +// carrying both a subset fence and the catch-all fence of the same group must +// have both rewritten in one pass (offsets are re-resolved by marker text +// after each splice, so the first replacement must not derail the second). +func TestRunGen_TwoFencesInOneFile(t *testing.T) { + dir := t.TempDir() + mk := func(verb string) skilldoc.Command { + return skilldoc.Command{Path: "svc " + verb, Group: "svc", Short: "S " + verb, Use: verb} + } + d := skilldoc.Dump{Commands: []skilldoc.Command{mk("list"), mk("rule-create"), mk("rule-delete")}} + + card := filepath.Join(dir, "reference", "svc.md") + writeFile(t, card, "# svc\n\nrules first\n\n"+ + skilldoc.FenceStart("svc[rule]")+"\n"+skilldoc.FenceEnd("svc[rule]")+"\n\nthen the rest\n\n"+ + skilldoc.FenceStart("svc")+"\n"+skilldoc.FenceEnd("svc")+"\n") + + if err := runGen(d, dir, "svc"); err != nil { + t.Fatalf("runGen: %v", err) + } + + body, err := os.ReadFile(card) + if err != nil { + t.Fatal(err) + } + got := string(body) + ruleAt := strings.Index(got, "### rule-create") + listAt := strings.Index(got, "### list") + if ruleAt < 0 || listAt < 0 || ruleAt > listAt { + t.Fatalf("both fences must be filled, subset before catch-all:\n%s", got) + } + if !strings.Contains(got, "rules first") || !strings.Contains(got, "then the rest") { + t.Errorf("gen clobbered hand-written content between fences:\n%s", got) + } + + var out bytes.Buffer + if n, _ := runCheck(d, dir, &out); n != 0 { + t.Errorf("after gen, check should be clean; got %d:\n%s", n, out.String()) + } +} diff --git a/internal/skilldoc/fence.go b/internal/skilldoc/fence.go new file mode 100644 index 0000000..6711080 --- /dev/null +++ b/internal/skilldoc/fence.go @@ -0,0 +1,181 @@ +package skilldoc + +// Fence topology: which GENERATED fence carries which commands of a group. +// +// A fence id is either the bare group name ("channel") — the group's +// catch-all fence — or the group plus a bracketed verb-prefix claim list +// ("channel[silence-rule,inhibit-rule]") — a subset fence that claims every +// verb starting with one of the prefixes. A group's fences may live in +// different cards; together they must cover the group exactly: every verb +// lands in exactly one fence, each prefix claims at least one verb, and any +// unclaimed remainder requires the catch-all fence to exist. + +import ( + "fmt" + "regexp" + "sort" + "strings" +) + +// FenceSpec is one parsed fence id. +type FenceSpec struct { + Group string + Prefixes []string // empty → the group's catch-all fence +} + +// ID renders the spec back to its marker id ("group" or "group[p1,p2]"). +func (s FenceSpec) ID() string { + if len(s.Prefixes) == 0 { + return s.Group + } + return s.Group + "[" + strings.Join(s.Prefixes, ",") + "]" +} + +// fenceIDRe accepts "group" or "group[prefix,prefix,...]". Group and prefix +// share the verb charset; no spaces, so a malformed claim list fails loudly +// instead of silently truncating at the first space. +var fenceIDRe = regexp.MustCompile(`^([a-z0-9-]+)(?:\[([a-z0-9-]+(?:,[a-z0-9-]+)*)\])?$`) + +// ParseFenceID parses a fence id as found in a GENERATED marker. +func ParseFenceID(id string) (FenceSpec, error) { + m := fenceIDRe.FindStringSubmatch(id) + if m == nil { + return FenceSpec{}, fmt.Errorf("malformed fence id %q (want group or group[verb-prefix,…])", id) + } + spec := FenceSpec{Group: m[1]} + if m[2] != "" { + spec.Prefixes = strings.Split(m[2], ",") + } + return spec, nil +} + +// FenceLoc is one GENERATED start marker found in a doc body. +type FenceLoc struct { + ID string + Offset int // byte offset of the start marker +} + +// fenceStartRe matches a start marker and captures its fence id; the literal +// " START " cannot appear in an end marker, so ends never match. +var fenceStartRe = regexp.MustCompile(`" ) -// GenerateFence renders the factual fenced block for one command group: a -// section per leaf verb with its short description and a flag table (name, -// type, required, usage + enum), plus a body-only (--data) note when the -// command has nested JSON-only fields, plus a one-line response-shape summary +// GenerateFence renders the fenced block for a group whose only fence is the +// catch-all — i.e. all of the group's commands in one block. Groups split +// across several cards must go through RenderGroupFences instead, which knows +// the sibling subset fences. +func GenerateFence(d Dump, group string) string { + out, _ := RenderGroupFences(d, group, []string{group}) + return out[group] +} + +// renderFence renders one fenced block: the id's markers around a section per +// command — each with its short description and a flag list (name, type, +// required, usage + enum), plus a body-only (--data) note when the command +// has nested JSON-only fields, plus a one-line response-shape summary // (top-level object vs. bare array vs. `{items: [...]}` page wrapper, and the // field names at that level) when the command documents one. Required-ness // and enums are sourced from the authoritative "Request fields:" text in each @@ -26,16 +35,15 @@ const ( // responseShapeLine), not re-derived or hand-curated. The flag list falls // back to the dump's Flags when no Request-fields block exists (read-only // verbs). Output is deterministic. -func GenerateFence(d Dump, group string) string { - cmds := groupCommands(d, group) - +func renderFence(id string, cmds []Command) string { var b strings.Builder - fmt.Fprintf(&b, fenceStartFmt+"\n\n", group) + fmt.Fprintf(&b, fenceStartFmt+"\n\n", id) // seenShapes maps a verbatim response-shape line to the name of the first - // command in THIS group (only — never across cards) that rendered it in - // full, so a later command with a byte-identical shape can point back at - // it instead of repeating the field list. Scoped to one GenerateFence call, - // so cards stay self-contained. + // command in THIS fence (only) that rendered it in full, so a later command + // with a byte-identical shape can point back at it instead of repeating the + // field list. Scoped to one rendered fence, not to the whole group: a group + // split into subset fences spans several cards, and a back-reference must + // never name a command the reader cannot see on the card in front of them. seenShapes := map[string]string{} for i, c := range cmds { if i > 0 { @@ -43,14 +51,15 @@ func GenerateFence(d Dump, group string) string { } writeCommand(&b, c, seenShapes) } - fmt.Fprintf(&b, "\n"+fenceEndFmt, group) + fmt.Fprintf(&b, "\n"+fenceEndFmt, id) return b.String() } -// FenceStart / FenceEnd return the literal markers for a group, used by the -// freshness check to locate fences in docs. -func FenceStart(group string) string { return fmt.Sprintf(fenceStartFmt, group) } -func FenceEnd(group string) string { return fmt.Sprintf(fenceEndFmt, group) } +// FenceStart / FenceEnd return the literal markers for a fence id (a bare +// group, or group[prefix,…] — see ParseFenceID), used to locate fences in +// docs. +func FenceStart(id string) string { return fmt.Sprintf(fenceStartFmt, id) } +func FenceEnd(id string) string { return fmt.Sprintf(fenceEndFmt, id) } func groupCommands(d Dump, group string) []Command { var cmds []Command diff --git a/internal/skilldoc/validate.go b/internal/skilldoc/validate.go index cfe298c..4c77bfe 100644 --- a/internal/skilldoc/validate.go +++ b/internal/skilldoc/validate.go @@ -16,7 +16,7 @@ type Doc struct { type Issue struct { Doc string Line int - Kind string // "unknown-command" | "unknown-flag" | "stale-fence" + Kind string // "unknown-command" | "unknown-flag" | "stale-fence" | "fence-topology" Detail string } @@ -47,37 +47,87 @@ func Validate(d Dump, docs []Doc) []Issue { return issues } -// CheckFences asserts every GENERATED: fence embedded in docs matches a -// fresh render from the dump. A fence whose inner content has drifted, or a -// start marker with no matching end marker, yields a stale-fence issue. Docs -// with no generated fence for a group are silently fine. +// CheckFences asserts every GENERATED fence embedded in docs matches a fresh +// render from the dump, and that each group's fences form a valid partition +// of the group's commands (see RenderGroupFences). A drifted fence or a start +// marker with no matching end marker yields a stale-fence issue; a malformed +// or unknown-group marker, and any partition violation, yields a +// fence-topology issue anchored at the group's first fence. func CheckFences(d Dump, docs []Doc) []Issue { + dumpGroups := map[string]bool{} + for _, g := range groups(d) { + dumpGroups[g] = true + } + + type loc struct { + doc string + body string + off int + id string + } var issues []Issue - for _, group := range groups(d) { - fresh := GenerateFence(d, group) - start, end := FenceStart(group), FenceEnd(group) - for _, doc := range docs { - si := strings.Index(doc.Body, start) - if si < 0 { - continue // no fence for this group in this doc + byGroup := map[string][]loc{} + for _, doc := range docs { + for _, fl := range FenceLocs(doc.Body) { + spec, err := ParseFenceID(fl.ID) + if err != nil { + issues = append(issues, Issue{ + Doc: doc.Path, + Line: lineOf(doc.Body, fl.Offset), + Kind: "fence-topology", + Detail: err.Error(), + }) + continue } - ei := strings.Index(doc.Body[si:], end) - if ei < 0 { + if !dumpGroups[spec.Group] { issues = append(issues, Issue{ Doc: doc.Path, - Line: lineOf(doc.Body, si), + Line: lineOf(doc.Body, fl.Offset), + Kind: "fence-topology", + Detail: "GENERATED:" + fl.ID + " names unknown command group " + spec.Group, + }) + continue + } + byGroup[spec.Group] = append(byGroup[spec.Group], loc{doc: doc.Path, body: doc.Body, off: fl.Offset, id: fl.ID}) + } + } + + // groups(d) is already sorted; every byGroup key is a member of it. + for _, group := range groups(d) { + locs, present := byGroup[group] + if !present { + continue + } + ids := make([]string, len(locs)) + for i, l := range locs { + ids[i] = l.id + } + rendered, violations := RenderGroupFences(d, group, ids) + for _, v := range violations { + issues = append(issues, Issue{ + Doc: locs[0].doc, + Line: lineOf(locs[0].body, locs[0].off), + Kind: "fence-topology", + Detail: v, + }) + } + for _, l := range locs { + start, end, ok := FindFence(l.body, l.id) + if !ok { + issues = append(issues, Issue{ + Doc: l.doc, + Line: lineOf(l.body, l.off), Kind: "stale-fence", - Detail: "unterminated GENERATED:" + group + " fence", + Detail: "unterminated GENERATED:" + l.id + " fence", }) continue } - block := doc.Body[si : si+ei+len(end)] - if block != fresh { + if fresh, rok := rendered[l.id]; rok && l.body[start:end] != fresh { issues = append(issues, Issue{ - Doc: doc.Path, - Line: lineOf(doc.Body, si), + Doc: l.doc, + Line: lineOf(l.body, l.off), Kind: "stale-fence", - Detail: "GENERATED:" + group + " fence is out of date — run `make gen-cards`", + Detail: "GENERATED:" + l.id + " fence is out of date — run `make gen-cards`", }) } } diff --git a/skills/flashduty/SKILL.md b/skills/flashduty/SKILL.md index dcc60a5..49ebcce 100644 --- a/skills/flashduty/SKILL.md +++ b/skills/flashduty/SKILL.md @@ -47,6 +47,8 @@ Configuration, permission-model, enrichment, monitor, and on-call questions are Read verbs (`list`, `get`, `info`, `detail`, `timeline`) are free. Mutating verbs (`create`, `update`, `delete`, `merge`, `ack`, `close`, `assign`, `move`, …) change state — recommend the action and get explicit per-target confirmation first. `merge` / `delete` are **irreversible** — double-check IDs. `create` notifies responders/subscribers. `list` before any bulk mutate to confirm the IDs. +**Keep an undo path — back up before overwriting.** Before `update` / `upsert` / `delete` on critical config (escalation policies, schedules, integrations, routing, status pages, alert rules, notification templates), fetch the current object with the matching read verb and save it to `backups/__.json` in your workspace. Your completion report must state: what changed, where the backup file is, and how to restore (re-upsert the saved JSON / delete the created IDs). For `create`-only changes skip the backup — report the undo (delete) instead. This is the only undo path for resources without server-side history: one read call buys recoverability. + ## Compound flows — bundled scripts Some asks span several commands. For those the skill ships a script that fetches everything in one call — run it as your **first action** for that ask, rather than hand-picking commands and writing the rest from memory: @@ -57,14 +59,17 @@ Some asks span several commands. For those the skill ships a script that fetches | intent / 意图 (terms route in either language) | card | |---|---| -| incident / fault / 故障 / 事件 / triage 分诊 / acknowledge 认领 / merge 合并 / escalate 升级 / postmortem 复盘 / **summarize or analyze an incident 故障汇总分析** | **`reference/incident.md`** | +| incident / fault / 故障 / 事件 / triage 分诊 / acknowledge 认领 / merge 合并 / escalate 升级 / **summarize or analyze an incident 故障汇总分析** | **`reference/incident.md`** | +| post-mortem / postmortem 复盘 / 复盘报告 / 复盘模板 / post-incident review / RCA report | **`reference/postmortem.md`** | | alert / 告警 / dedup 去重 / alert fields 告警字段 / alert pipeline 告警管道 | **`reference/alert.md`** | | change / 变更 / deployment 部署 / release 发布 / correlated change 变更关联 / what changed | **`reference/change.md`** | | monitor / 监控 / alert rule 告警规则 / datasource 数据源 / inspection 巡检 / rule config 规则配置 | **`reference/monit.md`** | | automation / 自动化 / 定时 AI SRE / scheduled AI task / daily brief / weekly report / webhook trigger / POST trigger / chat-created automation | **`reference/automation.md`** | | metric/log query / 指标查询 / 日志查询 / PromQL / LogsQL / SQL / trend 趋势 / log clustering 日志聚类 / datasource RCA 数据源排查 | **`reference/monit-query.md`** | | host diagnostics / 主机诊断 / on-box / process 进程 / load 负载 / lock 锁 / slow query 慢查询 / mysql / reachability 可达性 | **`reference/monit-agent.md`** | -| channel / 协作空间 / collaboration space / 频道 / integration 集成 / dispatch rule 分派规则 / escalation 升级规则 / noise reduction 降噪 / silence 静默 / inhibit 抑制 | **`reference/channel.md`** | +| channel / 协作空间 / collaboration space / 频道 / integration 集成 / alert grouping 告警分组 | **`reference/channel.md`** | +| dispatch rule 分派策略 / 分派规则 / escalation rule 升级规则 / notify layers 通知层级 / who gets paged | **`reference/escalation.md`** | +| silence 静默 / 屏蔽 / inhibit 抑制 / drop rule 丢弃 / noise reduction 降噪 / maintenance silence 维护窗口静默 | **`reference/noise.md`** | | enrichment / 数据加工 / 富化 / label mapping 字段映射 / extraction 提取 / mapping schema 集成 schema | **`reference/enrichment.md`** | | insight / 洞察 / stats 统计 / trend 趋势 / MTTA / MTTR / top alerts Top 告警 / incident export 故障导出 | **`reference/insight.md`** | | schedule / on-call / 值班 / 排班 / rotation 轮值 / who is on call 谁在值班 / shift 班次 / next responder 下一班 | **`reference/schedule.md`** | @@ -79,3 +84,5 @@ Some asks span several commands. For those the skill ships a script that fetches | sourcemap / source map / source mapping / symbolication / deobfuscate / stack enrich / dSYM / miniprogram source map | **`reference/sourcemap.md`** | | status page / 状态页 / public incident 公开事件 / public timeline 公开时间线 / maintenance window 维护窗口 / subscriber 订阅者 | **`reference/status-page.md`** | | AI-SRE platform / customize / 安装配置 MCP server (connector) 连接器 / install mcp / skill upload 上传技能 / A2A agent / session export 会话导出 | **`reference/safari.md`** | + +Shared reference: `reference/filters.md` — read it before composing any `filters` / `source_filters` / `target_filters` value (silence / inhibit / drop / escalation rules); it carries the condition shape, operators, and the valid key set per rule family. diff --git a/skills/flashduty/reference/alert.md b/skills/flashduty/reference/alert.md index cf6a40a..f0a5399 100644 --- a/skills/flashduty/reference/alert.md +++ b/skills/flashduty/reference/alert.md @@ -38,7 +38,7 @@ fduty alert feed --output-format toon fduty alert-event list --channel --since 1h --limit 30 --output-format toon ``` -Structured `alert-event list` output stays below 16 KiB. A trailing `...` means a long retained string was shortened. +Structured `alert-event list` output stays below 16 KiB. A trailing `...` means a long retained string was shortened. In json/toon mode rows default to the compact projection `event_id,alert_id,event_severity,event_status,event_time,title` (a stderr note says so when it applies); any other response field is one `--fields` away — a key missing from the output means it wasn't selected, not that the server omits it. ## Hot flow — merge noisy alerts into an existing incident @@ -148,7 +148,7 @@ View alert timeline `settings` shape depends on `kind`: `title_reset` → `{"title": "