#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn map_of(
pairs: impl IntoIterator<Item = (&'static str, Value)>,
) -> serde_json::Map<String, Value> {
pairs
.into_iter()
.map(|(name, schema)| (name.to_string(), schema))
.collect()
}
pub(super) fn properties(
parts: impl IntoIterator<Item = serde_json::Map<String, Value>>,
) -> serde_json::Map<String, Value> {
let mut out = serde_json::Map::new();
for part in parts {
out.extend(part);
}
out
}
pub(super) fn view_schema(required: &[&str], properties: serde_json::Map<String, Value>) -> Value {
json!({
"type": "object",
"additionalProperties": false,
"properties": properties,
"required": required,
})
}
pub(super) fn view_reference_schema() -> Value {
json!({
"type": "string",
"description": "Stable view id (`v-…`), or a display name when it is unambiguous. The id is the identity; the name is a label and may be changed or shared.",
})
}
pub(super) fn view_name_schema() -> Value {
json!({
"type": "string",
"description": "Display label. The stable id is generated by the registry and never changes.",
})
}
pub(super) fn view_plan_properties() -> serde_json::Map<String, Value> {
map_of([
(
"filter",
json!({
"type": "string",
"description": "The existing token query (`status:todo`, `label:urgent`, `category:done`, …), resolved by the same grammar `tix list --filter` uses.",
}),
),
(
"sort",
json!({
"type": "string",
"enum": crate::views::plan::SORT_KEYS,
"description": "The existing sort-key vocabulary.",
}),
),
(
"group",
json!({
"type": "string",
"enum": crate::views::plan::GROUP_KEYS,
"description": "The existing group-key vocabulary.",
}),
),
(
"hide_columns",
json!({
"type": "string",
"description": "Comma-separated board columns to hide when the view is applied. Giving this or `show_mismatch` makes visibility an absolute snapshot; omitting both means the view inherits the live board's visibility.",
}),
),
(
"show_mismatch",
json!({
"type": "boolean",
"description": "Show the computed `Mismatched` column when the view is applied.",
}),
),
])
}
pub(super) fn view_scope_properties() -> serde_json::Map<String, Value> {
map_of([
(
"scope",
json!({
"type": "string",
"enum": ["repository", "project", "user"],
"description": "Scope family. Defaults to `repository` when omitted.",
}),
),
(
"project",
json!({
"type": "string",
"description": "Project id, for `scope: \"project\"`. Unknown ids are refused by the registry, not by this schema.",
}),
),
(
"owner",
json!({
"type": "string",
"description": "Owner label, for `scope: \"user\"`. Metadata only — not an account and not a permission.",
}),
),
(
"shared_with",
json!({
"type": "string",
"description": "Comma-separated shared-with labels, for `scope: \"user\"`. Metadata only.",
}),
),
])
}
pub(super) fn bulk_schema(require_confirmation: bool) -> Value {
let caps = bulk::metadata::capabilities();
let mut properties = serde_json::Map::new();
for (key, description) in [
(
"select",
"Comma-separated issue selectors. Mutually exclusive with `filter` and `search` — name exactly one grammar.",
),
(
"filter",
"The existing structured filter grammar, e.g. `status:todo label:bug`. Mutually exclusive with `select` and `search`.",
),
(
"search",
"An existing search token, e.g. `fix crash`. Mutually exclusive with `select` and `filter`.",
),
(
"scope",
"Selection scope. Defaults to the active scope, exactly as the CLI's --scope does.",
),
(
"operation",
"One of the operation names `bulk_capabilities` reports.",
),
(
"target",
"The other end of a pair. Required for `link` and `unlink`; ignored for `edit` and `archive`.",
),
(
"type",
"The relationship kind. Required for `link` and `unlink`, and validated by the relationship service's own vocabulary — not this schema's.",
),
] {
properties.insert(
key.to_string(),
json!({ "type": "string", "description": description }),
);
}
for field in &caps.fields {
properties.insert(
field.name.clone(),
json!({
"type": "string",
"description": format!(
"{}{}",
field.summary,
if field.clearable {
" Clearable with `clear`."
} else {
" Not clearable."
}
)
}),
);
}
properties.insert(
"clear".to_string(),
json!({
"type": "string",
"description": format!(
"Name exactly one field to empty. One per call. Clearable: {}.",
caps.fields.iter().filter(|f| f.clearable).map(|f| f.name.as_str()).collect::<Vec<_>>().join(", ")
)
}),
);
let mut required = vec![Value::from("operation")];
if require_confirmation {
properties.insert(
"confirm".to_string(),
json!({
"type": "boolean",
"description": "Must be true. Without it nothing is written and the refusal names the missing confirmation (C-004)."
}),
);
required.push(Value::from("confirm"));
}
json!({
"type": "object",
"additionalProperties": false,
"properties": properties,
"required": required
})
}
pub(super) fn triage_schema(require_confirmation: bool) -> Value {
let mut properties = json!({
"issue": {
"type": ["string", "integer"],
"description": "One issue: a number, a filename fragment, or a unique title fragment. Mutually exclusive with `select` and `filter`.",
},
"select": {
"type": "string",
"description": "Comma-separated issue selectors: numbers, filename fragments, or unique title fragments. Mutually exclusive with `issue` and `filter`.",
},
"filter": {
"type": "string",
"description": "A `tix list --filter` query selecting the issues, e.g. `status:triage`. Mutually exclusive with `issue` and `select`.",
},
"decision": {
"type": "string",
"enum": ["accept", "decline", "duplicate", "defer"],
"description": "Which decision to make. `accept` may name a target column; `decline`, `duplicate` and `defer` require a reason.",
},
"reason": {
"type": "string",
"description": "Recorded on the History event. Required for decline, duplicate and defer — a missing one is a refused item naming `missing_reason`, exactly as the CLI reports it.",
},
"actor": {
"type": "string",
"description": "Who decided. Defaults to the git identity, exactly as the CLI's `--actor` does.",
},
"to": {
"type": "string",
"description": "The accept target column, validated by the board's own transition policy. With no `to`, the configured `[triage].accepted_status` is used, and with neither the item is refused rather than guessed.",
},
"of": {
"type": "string",
"description": "The duplicate target selector, for `decision: \"duplicate\"`. Creates a typed `duplicates` relation; the two files are never merged.",
},
});
let required = if require_confirmation {
properties["confirm"] = json!({
"type": "boolean",
"description": "Required, and must be true. A triage apply writes files and appends History; without it nothing is written.",
});
json!(["decision", "confirm"])
} else {
json!(["decision"])
};
json!({
"type": "object",
"additionalProperties": false,
"properties": properties,
"required": required,
})
}
pub(super) fn template_definition_schema(selector_key: &str) -> Value {
let selector = if selector_key == "id" {
json!({
"type": "string",
"description": "The new immutable stable ID: ASCII letters, digits, `-` or `_`, and not already in use.",
})
} else {
json!({
"type": "string",
"description": "Stable ID of the template to change, or a display name that names exactly one template.",
})
};
let mut properties = serde_json::Map::new();
properties.insert(selector_key.to_string(), selector);
properties.insert(
"name".to_string(),
json!({
"type": "string",
"description": "Mutable display label. Not identity: a rename keeps the stable ID and every stored part.",
}),
);
properties.insert(
"scope".to_string(),
json!({
"type": "string",
"description": "Explicit selection: `repository` (the default), `board:<name>` or `team:<label>`. There is no implicit inheritance, and a scope naming an unknown board or team is refused.",
}),
);
properties.insert("defaults".to_string(), template_defaults_schema());
properties.insert(
"prompts".to_string(),
json!({
"type": "array",
"description": "Ordered structured prompts. Supplying any prompt replaces the whole list.",
"items": template_prompt_schema(),
}),
);
properties.insert(
"references".to_string(),
json!({
"type": "array",
"description": "Typed references, validated against the existing registries before any issue is written. Supplying any reference replaces the whole list.",
"items": template_reference_schema(),
}),
);
Value::Object(properties)
}
pub(super) fn template_defaults_schema() -> Value {
let mut properties = serde_json::Map::new();
for (field, description) in [
("title", "Pre-filled title."),
("body", "Pre-filled Markdown body."),
(
"status",
"Pre-filled column; validated against the board's own columns.",
),
(
"priority",
"Pre-filled priority (`low` | `medium` | `high`).",
),
("assignee", "Pre-filled assignee identity."),
("estimate", "Pre-filled estimate."),
("reviewer", "Pre-filled reviewer."),
("start_date", "`YYYY-MM-DD`."),
("due_date", "`YYYY-MM-DD`; must be `>= start_date`."),
("parent", "Pre-filled parent selector."),
("type", "Pre-filled issue type (e.g. `bug`, `feature`)."),
] {
properties.insert(
field.to_string(),
json!({ "type": "string", "description": description }),
);
}
for (field, description) in [
("labels", "Pre-filled labels."),
("files", "Pre-filled attached file paths."),
("cycles", "Pre-filled cycle names."),
("milestones", "Pre-filled milestone names."),
("related_issues", "Pre-filled cross-repository references."),
] {
properties.insert(
field.to_string(),
json!({ "type": "array", "items": { "type": "string" }, "description": description }),
);
}
json!({
"type": "object",
"additionalProperties": false,
"description": "Normal create-request defaults, applied before user input.",
"properties": Value::Object(properties),
})
}
pub(super) fn template_prompt_schema() -> Value {
json!({
"type": "object",
"additionalProperties": false,
"properties": {
"key": { "type": "string", "description": "The normal create field this prompt answers. A key the create model does not know is refused." },
"label": { "type": "string", "description": "Display label; falls back to `key`." },
"kind": { "type": "string", "enum": ["text", "select", "multi-select", "date"], "description": "Input type — a type, never a behavior." },
"required": { "type": "boolean", "description": "Whether a value must be present before any issue is written." },
"default": { "type": "string", "description": "Merged before user input, and before the template's own default." },
"options_ref": { "type": "string", "enum": OPTIONS_SOURCES, "description": "An existing registry source for `select`/`multi-select` — never a new one." },
},
"required": ["key", "kind"],
})
}
pub(super) fn template_reference_schema() -> Value {
json!({
"type": "object",
"additionalProperties": false,
"properties": {
"kind": { "type": "string", "enum": ["label", "assignee", "file", "cycle", "milestone", "relationship"], "description": "The reference kind." },
"value": { "type": "string", "description": "The referenced value: a label, identity, path, name, or relation kind." },
"required": { "type": "boolean", "description": "Whether absence rejects resolution." },
},
"required": ["kind", "value"],
})
}