ticket: remove legacy schema fields
This commit is contained in:
+21
-64
@@ -23,7 +23,7 @@ const STATUSES: [TicketStatus; 3] = [
|
||||
TicketStatus::Pending,
|
||||
TicketStatus::Closed,
|
||||
];
|
||||
const REQUIRED_FIELDS: [&str; 11] = [
|
||||
const REQUIRED_FIELDS: [&str; 10] = [
|
||||
"id",
|
||||
"slug",
|
||||
"title",
|
||||
@@ -34,7 +34,6 @@ const REQUIRED_FIELDS: [&str; 11] = [
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"assignee",
|
||||
"legacy_ticket",
|
||||
];
|
||||
const MAX_STATE_CHANGE_REASON_BYTES: usize = 1024;
|
||||
const MAX_INTAKE_SUMMARY_BODY_BYTES: usize = 16 * 1024;
|
||||
@@ -196,7 +195,7 @@ impl TicketWorkflowState {
|
||||
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"planning" | "intake" => Some(Self::Planning),
|
||||
"planning" => Some(Self::Planning),
|
||||
"ready" => Some(Self::Ready),
|
||||
"queued" => Some(Self::Queued),
|
||||
"inprogress" => Some(Self::InProgress),
|
||||
@@ -496,10 +495,7 @@ pub struct NewTicket {
|
||||
pub body: MarkdownText,
|
||||
pub author: Option<String>,
|
||||
pub assignee: Option<String>,
|
||||
pub legacy_ticket: Option<String>,
|
||||
pub readiness: Option<String>,
|
||||
/// Legacy metadata accepted for existing records only; not a workflow stop gate.
|
||||
pub needs_preflight: Option<bool>,
|
||||
pub risk_flags: Vec<String>,
|
||||
pub action_required: Option<String>,
|
||||
pub workflow_state: Option<TicketWorkflowState>,
|
||||
@@ -519,9 +515,7 @@ impl NewTicket {
|
||||
body: MarkdownText::new(DEFAULT_TICKET_BODY),
|
||||
author: None,
|
||||
assignee: None,
|
||||
legacy_ticket: None,
|
||||
readiness: None,
|
||||
needs_preflight: None,
|
||||
risk_flags: Vec::new(),
|
||||
action_required: None,
|
||||
workflow_state: None,
|
||||
@@ -568,10 +562,7 @@ pub struct TicketMeta {
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub assignee: Option<String>,
|
||||
pub legacy_ticket: Option<String>,
|
||||
pub readiness: Option<String>,
|
||||
/// Legacy metadata accepted for existing records only; not a workflow stop gate.
|
||||
pub needs_preflight: Option<bool>,
|
||||
pub risk_flags: Vec<String>,
|
||||
pub action_required: Option<String>,
|
||||
pub workflow_state: TicketWorkflowState,
|
||||
@@ -592,8 +583,6 @@ pub struct TicketSummary {
|
||||
pub priority: String,
|
||||
pub labels: Vec<String>,
|
||||
pub readiness: Option<String>,
|
||||
/// Legacy metadata accepted for existing records only; not a workflow stop gate.
|
||||
pub needs_preflight: Option<bool>,
|
||||
pub action_required: Option<String>,
|
||||
pub workflow_state: TicketWorkflowState,
|
||||
pub workflow_state_explicit: bool,
|
||||
@@ -1053,7 +1042,6 @@ impl TicketBackend for LocalTicketBackend {
|
||||
priority: meta.priority,
|
||||
labels: meta.labels,
|
||||
readiness: meta.readiness,
|
||||
needs_preflight: meta.needs_preflight,
|
||||
action_required: meta.action_required,
|
||||
workflow_state: meta.workflow_state,
|
||||
workflow_state_explicit: meta.workflow_state_explicit,
|
||||
@@ -1152,19 +1140,12 @@ impl TicketBackend for LocalTicketBackend {
|
||||
"assignee".to_string(),
|
||||
yaml_string_or_null(input.assignee.as_deref()),
|
||||
));
|
||||
fields.push((
|
||||
"legacy_ticket".to_string(),
|
||||
yaml_string_or_null(input.legacy_ticket.as_deref()),
|
||||
));
|
||||
if let Some(readiness) = input.readiness {
|
||||
fields.push((
|
||||
"readiness".to_string(),
|
||||
format_yaml_string_scalar(readiness.as_str()),
|
||||
));
|
||||
}
|
||||
if let Some(needs_preflight) = input.needs_preflight {
|
||||
fields.push(("needs_preflight".to_string(), needs_preflight.to_string()));
|
||||
}
|
||||
if !input.risk_flags.is_empty() {
|
||||
fields.push(("risk_flags".to_string(), labels_yaml(&input.risk_flags)));
|
||||
}
|
||||
@@ -1668,9 +1649,7 @@ struct TicketItemFrontmatter {
|
||||
created_at: Option<String>,
|
||||
updated_at: Option<String>,
|
||||
assignee: Option<String>,
|
||||
legacy_ticket: Option<String>,
|
||||
readiness: Option<String>,
|
||||
needs_preflight: Option<bool>,
|
||||
risk_flags: Vec<String>,
|
||||
action_required: Option<String>,
|
||||
workflow_state: Option<TicketWorkflowState>,
|
||||
@@ -1753,7 +1732,7 @@ fn parse_ticket_frontmatter(content: &str) -> std::result::Result<TicketItemFron
|
||||
let workflow_state_value = yaml_string(&mapping, "workflow_state")?;
|
||||
let workflow_state = match workflow_state_value.as_deref() {
|
||||
Some(value) => Some(TicketWorkflowState::parse(value).ok_or_else(|| {
|
||||
format!("invalid workflow_state '{value}': expected planning, ready, queued, inprogress, done, or legacy intake")
|
||||
format!("invalid workflow_state '{value}': expected planning, ready, queued, inprogress, or done")
|
||||
})?),
|
||||
None => None,
|
||||
};
|
||||
@@ -1769,9 +1748,7 @@ fn parse_ticket_frontmatter(content: &str) -> std::result::Result<TicketItemFron
|
||||
created_at: yaml_string(&mapping, "created_at")?,
|
||||
updated_at: yaml_string(&mapping, "updated_at")?,
|
||||
assignee: yaml_string(&mapping, "assignee")?,
|
||||
legacy_ticket: yaml_string(&mapping, "legacy_ticket")?,
|
||||
readiness: yaml_string(&mapping, "readiness")?,
|
||||
needs_preflight: yaml_bool(&mapping, "needs_preflight")?,
|
||||
risk_flags: yaml_string_list(&mapping, "risk_flags")?,
|
||||
action_required: yaml_string(&mapping, "action_required")?,
|
||||
workflow_state,
|
||||
@@ -1802,17 +1779,6 @@ fn yaml_string(mapping: &YamlMapping, key: &str) -> std::result::Result<Option<S
|
||||
}
|
||||
}
|
||||
|
||||
fn yaml_bool(mapping: &YamlMapping, key: &str) -> std::result::Result<Option<bool>, String> {
|
||||
match yaml_get(mapping, key) {
|
||||
Some(YamlValue::Null) | None => Ok(None),
|
||||
Some(YamlValue::Bool(value)) => Ok(Some(*value)),
|
||||
Some(value) => Err(format!(
|
||||
"frontmatter field `{key}` must be a YAML boolean or null, found {}",
|
||||
yaml_kind(value)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn yaml_string_list(mapping: &YamlMapping, key: &str) -> std::result::Result<Vec<String>, String> {
|
||||
match yaml_get(mapping, key) {
|
||||
Some(YamlValue::Null) | None => Ok(Vec::new()),
|
||||
@@ -1888,9 +1854,7 @@ fn ticket_meta(frontmatter: TicketItemFrontmatter) -> TicketMeta {
|
||||
created_at: frontmatter.created_at,
|
||||
updated_at: frontmatter.updated_at,
|
||||
assignee: frontmatter.assignee,
|
||||
legacy_ticket: frontmatter.legacy_ticket,
|
||||
readiness: frontmatter.readiness,
|
||||
needs_preflight: frontmatter.needs_preflight,
|
||||
risk_flags: frontmatter.risk_flags,
|
||||
action_required: frontmatter.action_required,
|
||||
workflow_state,
|
||||
@@ -2477,15 +2441,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_state_parses_legacy_intake_as_planning_and_emits_planning() {
|
||||
fn workflow_state_rejects_legacy_intake_alias() {
|
||||
assert_eq!(
|
||||
TicketWorkflowState::parse("planning"),
|
||||
Some(TicketWorkflowState::Planning)
|
||||
);
|
||||
assert_eq!(
|
||||
TicketWorkflowState::parse("intake"),
|
||||
Some(TicketWorkflowState::Planning)
|
||||
);
|
||||
assert_eq!(TicketWorkflowState::parse("intake"), None);
|
||||
assert_eq!(TicketWorkflowState::Planning.as_str(), "planning");
|
||||
assert_eq!(
|
||||
TicketWorkflowState::default_for_status(&ExtensibleTicketStatus::Open),
|
||||
@@ -2538,9 +2499,7 @@ labels: [ticket, backend]
|
||||
created_at: 2026-06-05T00:00:00Z
|
||||
updated_at: 2026-06-05T00:00:00Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
readiness: implementation-ready
|
||||
needs_preflight: false
|
||||
risk_flags: [low, local]
|
||||
action_required: none
|
||||
workflow_state: ready
|
||||
@@ -2556,7 +2515,6 @@ queued_at: 2026-06-05T00:01:00Z
|
||||
assert_eq!(meta.id, "20260605-000000-example");
|
||||
assert_eq!(meta.labels, vec!["ticket", "backend"]);
|
||||
assert_eq!(meta.readiness.as_deref(), Some("implementation-ready"));
|
||||
assert_eq!(meta.needs_preflight, Some(false));
|
||||
assert_eq!(meta.risk_flags, vec!["low", "local"]);
|
||||
assert_eq!(meta.action_required.as_deref(), Some("none"));
|
||||
assert_eq!(meta.workflow_state, TicketWorkflowState::Ready);
|
||||
@@ -2567,19 +2525,17 @@ queued_at: 2026-06-05T00:01:00Z
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yaml_frontmatter_preserves_typed_nulls_lists_bools_and_quoted_strings() {
|
||||
fn yaml_frontmatter_preserves_typed_nulls_lists_and_quoted_strings() {
|
||||
let frontmatter = parse_ticket_frontmatter(
|
||||
r#"labels:
|
||||
- ticket
|
||||
- backend
|
||||
risk_flags: [low, local]
|
||||
assignee: ~
|
||||
legacy_ticket:
|
||||
attention_required: null
|
||||
action_required: "null"
|
||||
readiness: "~"
|
||||
needs_preflight: false
|
||||
workflow_state: intake
|
||||
workflow_state: planning
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -2587,11 +2543,9 @@ workflow_state: intake
|
||||
assert_eq!(meta.labels, vec!["ticket", "backend"]);
|
||||
assert_eq!(meta.risk_flags, vec!["low", "local"]);
|
||||
assert_eq!(meta.assignee, None);
|
||||
assert_eq!(meta.legacy_ticket, None);
|
||||
assert_eq!(meta.attention_required, None);
|
||||
assert_eq!(meta.action_required.as_deref(), Some("null"));
|
||||
assert_eq!(meta.readiness.as_deref(), Some("~"));
|
||||
assert_eq!(meta.needs_preflight, Some(false));
|
||||
assert_eq!(meta.workflow_state, TicketWorkflowState::Planning);
|
||||
assert!(meta.workflow_state_explicit);
|
||||
}
|
||||
@@ -2604,17 +2558,17 @@ workflow_state: intake
|
||||
"{labels_error}"
|
||||
);
|
||||
|
||||
let bool_error = parse_ticket_frontmatter("needs_preflight: 1").unwrap_err();
|
||||
assert!(
|
||||
bool_error.contains("must be a YAML boolean"),
|
||||
"{bool_error}"
|
||||
);
|
||||
|
||||
let workflow_error = parse_ticket_frontmatter("workflow_state: almost").unwrap_err();
|
||||
assert!(
|
||||
workflow_error.contains("invalid workflow_state"),
|
||||
"{workflow_error}"
|
||||
);
|
||||
|
||||
let intake_error = parse_ticket_frontmatter("workflow_state: intake").unwrap_err();
|
||||
assert!(
|
||||
intake_error.contains("invalid workflow_state"),
|
||||
"{intake_error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2635,6 +2589,9 @@ workflow_state: intake
|
||||
assert!(dir.join("thread.md").exists());
|
||||
assert!(dir.join("artifacts/.gitkeep").exists());
|
||||
assert_eq!(ticket.slug, "example-ticket");
|
||||
let item = fs::read_to_string(dir.join("item.md")).unwrap();
|
||||
assert!(!item.contains("legacy_ticket:"));
|
||||
assert!(!item.contains("needs_preflight:"));
|
||||
let record = backend.show(TicketIdOrSlug::Id(ticket.id.clone())).unwrap();
|
||||
assert_eq!(record.meta.workflow_state, TicketWorkflowState::Planning);
|
||||
assert!(record.meta.workflow_state_explicit);
|
||||
@@ -3055,7 +3012,7 @@ workflow_state: intake
|
||||
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
|
||||
fs::write(
|
||||
root.join("open/bad/item.md"),
|
||||
"---\nid: bad\nslug: bad\ntitle: Bad\nstatus: open\nkind: task\npriority: P2\nworkflow_state: almost\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\nlegacy_ticket: null\n---\n",
|
||||
"---\nid: bad\nslug: bad\ntitle: Bad\nstatus: open\nkind: task\npriority: P2\nworkflow_state: almost\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(root.join("open/bad/thread.md"), "").unwrap();
|
||||
@@ -3080,7 +3037,7 @@ workflow_state: intake
|
||||
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
|
||||
fs::write(
|
||||
root.join("open/bad/item.md"),
|
||||
"---\nid: bad\nslug: bad\ntitle: Bad\nstatus: open\nkind: task\npriority: P2\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\nlegacy_ticket: null\n---\n",
|
||||
"---\nid: bad\nslug: bad\ntitle: Bad\nstatus: open\nkind: task\npriority: P2\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
@@ -3110,7 +3067,7 @@ workflow_state: intake
|
||||
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
|
||||
fs::write(
|
||||
root.join("open/bad/item.md"),
|
||||
"---\nid: other\nslug: dup\ntitle: Bad\nstatus: pending\nkind: task\npriority: P2\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\nlegacy_ticket: null\n---\n",
|
||||
"---\nid: other\nslug: dup\ntitle: Bad\nstatus: pending\nkind: task\npriority: P2\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
@@ -3121,7 +3078,7 @@ workflow_state: intake
|
||||
fs::create_dir_all(root.join("pending/other/artifacts")).unwrap();
|
||||
fs::write(
|
||||
root.join("pending/other/item.md"),
|
||||
"---\nid: other\nslug: dup\ntitle: Dup\nstatus: pending\nkind: task\npriority: P2\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\nlegacy_ticket: null\n---\n",
|
||||
"---\nid: other\nslug: dup\ntitle: Dup\nstatus: pending\nkind: task\npriority: P2\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(root.join("pending/other/thread.md"), "").unwrap();
|
||||
@@ -3167,7 +3124,7 @@ workflow_state: intake
|
||||
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
|
||||
fs::write(
|
||||
root.join("open/bad/item.md"),
|
||||
"---\nid: ../bad\nslug: bad\ntitle: Bad\nstatus: open\nkind: task\npriority: P2\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\nlegacy_ticket: null\n---\n",
|
||||
"---\nid: ../bad\nslug: bad\ntitle: Bad\nstatus: open\nkind: task\npriority: P2\nlabels: []\ncreated_at: x\nupdated_at: x\nassignee: null\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(root.join("open/bad/thread.md"), "").unwrap();
|
||||
|
||||
+23
-12
@@ -110,9 +110,6 @@ struct TicketCreateParams {
|
||||
/// Optional assignee frontmatter value.
|
||||
#[serde(default)]
|
||||
assignee: Option<String>,
|
||||
/// Optional legacy ticket reference frontmatter value.
|
||||
#[serde(default)]
|
||||
legacy_ticket: Option<String>,
|
||||
/// Optional readiness frontmatter value.
|
||||
#[serde(default)]
|
||||
readiness: Option<String>,
|
||||
@@ -139,7 +136,6 @@ struct TicketCreateParams {
|
||||
#[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum TicketWorkflowStateParam {
|
||||
#[serde(alias = "intake")]
|
||||
Planning,
|
||||
Ready,
|
||||
Queued,
|
||||
@@ -409,7 +405,6 @@ impl Tool for TicketCreateTool {
|
||||
}
|
||||
input.author = params.author;
|
||||
input.assignee = params.assignee;
|
||||
input.legacy_ticket = params.legacy_ticket;
|
||||
input.readiness = params.readiness;
|
||||
input.risk_flags = params.risk_flags;
|
||||
input.action_required = params.action_required;
|
||||
@@ -757,7 +752,6 @@ fn ticket_summary_json(ticket: TicketSummary) -> Value {
|
||||
"priority": ticket.priority,
|
||||
"labels": ticket.labels,
|
||||
"readiness": ticket.readiness,
|
||||
"needs_preflight": ticket.needs_preflight,
|
||||
"action_required": ticket.action_required,
|
||||
"workflow_state": ticket.workflow_state.as_str(),
|
||||
"workflow_state_explicit": ticket.workflow_state_explicit,
|
||||
@@ -814,9 +808,7 @@ fn ticket_json(
|
||||
"created_at": ticket.meta.created_at,
|
||||
"updated_at": ticket.meta.updated_at,
|
||||
"assignee": ticket.meta.assignee,
|
||||
"legacy_ticket": ticket.meta.legacy_ticket,
|
||||
"readiness": ticket.meta.readiness,
|
||||
"needs_preflight": ticket.meta.needs_preflight,
|
||||
"risk_flags": ticket.meta.risk_flags,
|
||||
"action_required": ticket.meta.action_required,
|
||||
"workflow_state": ticket.meta.workflow_state.as_str(),
|
||||
@@ -1068,20 +1060,29 @@ mod tests {
|
||||
assert!(created.summary.contains("Created ticket"));
|
||||
let created_json: Value = serde_json::from_str(&created.content.unwrap()).unwrap();
|
||||
let id = created_json["id"].as_str().unwrap().to_string();
|
||||
let created_text = created_json.to_string();
|
||||
assert!(!created_text.contains("legacy_ticket"));
|
||||
assert!(!created_text.contains("needs_preflight"));
|
||||
|
||||
let listed = list
|
||||
.execute(&json!({ "status": "open", "label": "tool" }).to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(listed.summary.contains("Listed 1 ticket"));
|
||||
assert!(listed.content.unwrap().contains("Tool Created"));
|
||||
let listed_content = listed.content.unwrap();
|
||||
assert!(listed_content.contains("Tool Created"));
|
||||
assert!(!listed_content.contains("legacy_ticket"));
|
||||
assert!(!listed_content.contains("needs_preflight"));
|
||||
|
||||
let shown = show
|
||||
.execute(&json!({ "id": id, "event_limit": 10 }).to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(shown.summary.contains("tool-created"));
|
||||
assert!(shown.content.unwrap().contains("Created by tool"));
|
||||
let shown_content = shown.content.unwrap();
|
||||
assert!(shown_content.contains("Created by tool"));
|
||||
assert!(!shown_content.contains("legacy_ticket"));
|
||||
assert!(!shown_content.contains("needs_preflight"));
|
||||
|
||||
let report = doctor.execute(&json!({}).to_string()).await.unwrap();
|
||||
assert!(report.summary.contains("0 error(s)"));
|
||||
@@ -1370,7 +1371,7 @@ mod tests {
|
||||
&json!({
|
||||
"ticket": done.id,
|
||||
"from": "done",
|
||||
"to": "intake",
|
||||
"to": "planning",
|
||||
"reason": "backwards",
|
||||
"body": "Should not move backwards.\n"
|
||||
})
|
||||
@@ -1459,7 +1460,17 @@ mod tests {
|
||||
#[test]
|
||||
fn ticket_tool_definitions_have_expected_names_and_schemas() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let names = ticket_tools(backend(&temp))
|
||||
let tools = ticket_tools(backend(&temp));
|
||||
let create_schema = tools
|
||||
.iter()
|
||||
.map(|definition| definition().0)
|
||||
.find(|meta| meta.name == "TicketCreate")
|
||||
.unwrap()
|
||||
.input_schema
|
||||
.to_string();
|
||||
assert!(!create_schema.contains("legacy_ticket"));
|
||||
assert!(!create_schema.contains("needs_preflight"));
|
||||
let names = tools
|
||||
.into_iter()
|
||||
.map(|definition| definition().0)
|
||||
.map(|meta| {
|
||||
|
||||
Reference in New Issue
Block a user