ticket: collapse ticket access into one feature

This commit is contained in:
2026-07-20 19:51:46 +09:00
parent 2c01e7672b
commit 064965d350
13 changed files with 147 additions and 280 deletions
+8 -31
View File
@@ -87,8 +87,6 @@ pub struct FeatureConfigPartial {
#[serde(default)]
pub ticket: Option<TicketFeatureConfigPartial>,
#[serde(default)]
pub ticket_orchestration: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub plugins: Option<FeatureFlagConfigPartial>,
}
@@ -100,11 +98,6 @@ impl FeatureConfigPartial {
web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge),
workers: merge_option(self.workers, other.workers, FeatureFlagConfigPartial::merge),
ticket: merge_option(self.ticket, other.ticket, TicketFeatureConfigPartial::merge),
ticket_orchestration: merge_option(
self.ticket_orchestration,
other.ticket_orchestration,
FeatureFlagConfigPartial::merge,
),
plugins: merge_option(self.plugins, other.plugins, FeatureFlagConfigPartial::merge),
}
}
@@ -128,10 +121,6 @@ impl FeatureFlagConfigPartial {
pub struct TicketFeatureConfigPartial {
#[serde(default)]
pub enabled: Option<bool>,
/// Legacy access field. Prefer `preset` for new profile/DCDL authoring.
#[serde(default)]
pub access: Option<TicketFeatureAccessConfig>,
/// Semantic Ticket access preset for profile/DCDL authoring.
#[serde(default)]
pub preset: Option<TicketFeatureAccessConfig>,
}
@@ -140,7 +129,6 @@ impl TicketFeatureConfigPartial {
fn merge(self, other: Self) -> Self {
Self {
enabled: other.enabled.or(self.enabled),
access: other.access.or(self.access),
preset: other.preset.or(self.preset),
}
}
@@ -163,10 +151,6 @@ impl From<FeatureConfigPartial> for FeatureConfig {
.ticket
.map(TicketFeatureConfig::from)
.unwrap_or_default(),
ticket_orchestration: value
.ticket_orchestration
.map(FeatureFlagConfig::from)
.unwrap_or_default(),
plugins: value
.plugins
.map(FeatureFlagConfig::from)
@@ -195,7 +179,7 @@ impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
fn from(value: TicketFeatureConfigPartial) -> Self {
Self {
enabled: value.enabled.unwrap_or_default(),
access: value.preset.or(value.access).unwrap_or_default(),
preset: value.preset.unwrap_or_default(),
}
}
}
@@ -204,8 +188,7 @@ impl From<TicketFeatureConfig> for TicketFeatureConfigPartial {
fn from(value: TicketFeatureConfig) -> Self {
Self {
enabled: Some(value.enabled),
access: Some(value.access),
preset: None,
preset: Some(value.preset),
}
}
}
@@ -218,7 +201,6 @@ impl From<FeatureConfig> for FeatureConfigPartial {
web: Some(value.web.into()),
workers: Some(value.workers.into()),
ticket: Some(value.ticket.into()),
ticket_orchestration: Some(value.ticket_orchestration.into()),
plugins: Some(value.plugins.into()),
}
}
@@ -1764,7 +1746,6 @@ worker_max_turns = 7
assert!(!manifest.feature.web.enabled);
assert!(!manifest.feature.workers.enabled);
assert!(!manifest.feature.ticket.enabled);
assert!(!manifest.feature.ticket_orchestration.enabled);
}
#[test]
@@ -1776,10 +1757,7 @@ enabled = true
[feature.ticket]
enabled = true
access = "read_only"
[feature.ticket_orchestration]
enabled = true
preset = "read_only"
"#,
)
.unwrap();
@@ -1810,10 +1788,9 @@ enabled = true
assert!(manifest.feature.task.enabled);
assert!(manifest.feature.ticket.enabled);
assert_eq!(
manifest.feature.ticket.access,
manifest.feature.ticket.preset,
TicketFeatureAccessConfig::ReadOnly
);
assert!(manifest.feature.ticket_orchestration.enabled);
assert!(!manifest.feature.memory.enabled);
}
@@ -1826,14 +1803,14 @@ enabled = true
[feature.ticket]
enabled = true
access = "read_only"
preset = "read_only"
"#,
)
.unwrap();
let upper = WorkerManifestConfig::from_toml(
r#"
[feature.ticket]
access = "lifecycle"
preset = "orchestration_control"
[feature.web]
enabled = true
@@ -1868,8 +1845,8 @@ enabled = true
assert!(manifest.feature.memory.enabled);
assert!(manifest.feature.ticket.enabled);
assert_eq!(
manifest.feature.ticket.access,
TicketFeatureAccessConfig::Lifecycle
manifest.feature.ticket.preset,
TicketFeatureAccessConfig::OrchestrationControl
);
assert!(manifest.feature.web.enabled);
assert!(!manifest.feature.workers.enabled);
+3 -11
View File
@@ -115,8 +115,6 @@ pub struct FeatureConfig {
#[serde(default)]
pub ticket: TicketFeatureConfig,
#[serde(default)]
pub ticket_orchestration: FeatureFlagConfig,
#[serde(default)]
pub plugins: FeatureFlagConfig,
}
@@ -128,7 +126,6 @@ impl Default for FeatureConfig {
web: FeatureFlagConfig::disabled(),
workers: FeatureFlagConfig::disabled(),
ticket: TicketFeatureConfig::default(),
ticket_orchestration: FeatureFlagConfig::disabled(),
plugins: FeatureFlagConfig::disabled(),
}
}
@@ -160,18 +157,15 @@ impl Default for FeatureFlagConfig {
pub struct TicketFeatureConfig {
#[serde(default)]
pub enabled: bool,
/// Which non-orchestration Ticket surface to expose when `enabled = true`.
/// Orchestration-plan/relation tools are controlled independently by
/// `[feature.ticket_orchestration].enabled`.
#[serde(default)]
pub access: TicketFeatureAccessConfig,
pub preset: TicketFeatureAccessConfig,
}
impl Default for TicketFeatureConfig {
fn default() -> Self {
Self {
enabled: false,
access: TicketFeatureAccessConfig::Lifecycle,
preset: TicketFeatureAccessConfig::default(),
}
}
}
@@ -185,13 +179,11 @@ pub enum TicketFeatureAccessConfig {
OrchestrationControl,
WorkReport,
Review,
/// Legacy broad mutation preset retained as a migration shim.
Lifecycle,
}
impl Default for TicketFeatureAccessConfig {
fn default() -> Self {
Self::Lifecycle
Self::WorkspaceAuthoring
}
}
+11 -30
View File
@@ -894,7 +894,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true,
true,
true,
false,
);
Some(value)
}
@@ -908,7 +907,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true,
true,
false,
false,
);
Some(value)
}
@@ -922,7 +920,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true,
true,
true,
true,
);
Some(value)
}
@@ -936,7 +933,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true,
true,
false,
false,
);
Some(value)
}
@@ -950,7 +946,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true,
true,
false,
false,
);
Some(value)
}
@@ -976,8 +971,7 @@ fn builtin_default_profile_artifact() -> serde_json::Value {
"memory": { "enabled": true },
"web": { "enabled": true },
"workers": { "enabled": true },
"ticket": { "enabled": true, "access": "lifecycle" },
"ticket_orchestration": { "enabled": false }
"ticket": { "enabled": true, "preset": "workspace_authoring" }
},
"memory": {
"extract_threshold": 50000,
@@ -1004,7 +998,6 @@ fn apply_role_profile(
memory: bool,
web: bool,
workers: bool,
ticket_orchestration: bool,
) {
value["slug"] = serde_json::Value::String(slug.to_string());
value["description"] = serde_json::Value::String(description.to_string());
@@ -1018,14 +1011,12 @@ fn apply_role_profile(
"orchestrator" => TicketFeatureAccessConfig::OrchestrationControl,
"coder" => TicketFeatureAccessConfig::WorkReport,
"reviewer" => TicketFeatureAccessConfig::Review,
_ => TicketFeatureAccessConfig::Lifecycle,
_ => TicketFeatureAccessConfig::WorkspaceAuthoring,
};
value["feature"]["ticket"] = serde_json::json!({
"enabled": true,
"preset": ticket_access,
});
value["feature"]["ticket_orchestration"] =
serde_json::json!({ "enabled": ticket_orchestration });
}
fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> {
@@ -1452,10 +1443,9 @@ mod tests {
assert!(companion.web.is_some());
assert!(companion.feature.ticket.enabled);
assert_eq!(
companion.feature.ticket.access,
companion.feature.ticket.preset,
TicketFeatureAccessConfig::WorkspaceAuthoring
);
assert!(!companion.feature.ticket_orchestration.enabled);
assert_eq!(
companion.compaction.as_ref().unwrap().threshold,
Some(240000)
@@ -1478,7 +1468,7 @@ mod tests {
assert!(!intake.feature.workers.enabled);
assert!(intake.feature.ticket.enabled);
assert_eq!(
intake.feature.ticket.access,
intake.feature.ticket.preset,
TicketFeatureAccessConfig::Intake
);
assert!(intake.scope.allow.is_empty());
@@ -1486,17 +1476,15 @@ mod tests {
assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
assert!(intake.web.is_some());
assert!(intake.compaction.is_some());
assert!(!intake.feature.ticket_orchestration.enabled);
let orchestrator = resolve("orchestrator");
assert!(orchestrator.feature.task.enabled);
assert!(orchestrator.feature.workers.enabled);
assert!(orchestrator.feature.ticket.enabled);
assert_eq!(
orchestrator.feature.ticket.access,
orchestrator.feature.ticket.preset,
TicketFeatureAccessConfig::OrchestrationControl
);
assert!(orchestrator.feature.ticket_orchestration.enabled);
assert!(orchestrator.scope.allow.is_empty());
assert!(orchestrator.delegation_scope.allow.is_empty());
assert_eq!(
@@ -1516,20 +1504,18 @@ mod tests {
assert!(coder.compaction.is_some());
assert!(coder.feature.ticket.enabled);
assert_eq!(
coder.feature.ticket.access,
coder.feature.ticket.preset,
TicketFeatureAccessConfig::WorkReport
);
assert!(!coder.feature.ticket_orchestration.enabled);
let reviewer = resolve("reviewer");
assert!(reviewer.feature.task.enabled);
assert!(!reviewer.feature.workers.enabled);
assert!(reviewer.feature.ticket.enabled);
assert_eq!(
reviewer.feature.ticket.access,
reviewer.feature.ticket.preset,
TicketFeatureAccessConfig::Review
);
assert!(!reviewer.feature.ticket_orchestration.enabled);
assert!(reviewer.scope.allow.is_empty());
assert!(reviewer.delegation_scope.allow.is_empty());
assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
@@ -1678,10 +1664,7 @@ enabled = true
[feature.ticket]
enabled = true
access = "read_only"
[feature.ticket_orchestration]
enabled = false
preset = "read_only"
"#,
);
let workspace = tmp.path().join("workspace");
@@ -1700,10 +1683,9 @@ enabled = false
assert!(resolved.manifest.feature.workers.enabled);
assert!(resolved.manifest.feature.ticket.enabled);
assert_eq!(
resolved.manifest.feature.ticket.access,
resolved.manifest.feature.ticket.preset,
crate::TicketFeatureAccessConfig::ReadOnly
);
assert!(!resolved.manifest.feature.ticket_orchestration.enabled);
assert_eq!(
resolved.manifest.delegation_scope.allow[0].target,
workspace
@@ -1791,10 +1773,9 @@ worker_context_max_tokens = 68000
assert!(resolved.manifest.session.record_event_trace);
assert!(resolved.manifest.feature.ticket.enabled);
assert_eq!(
resolved.manifest.feature.ticket.access,
crate::TicketFeatureAccessConfig::Lifecycle
resolved.manifest.feature.ticket.preset,
crate::TicketFeatureAccessConfig::WorkspaceAuthoring
);
assert!(!resolved.manifest.feature.ticket_orchestration.enabled);
assert_eq!(
resolved.profile.as_ref().unwrap().name.as_deref(),
Some("default")