profile: remove extend profile composition
This commit is contained in:
parent
f709fc1000
commit
7c6070ef2f
|
|
@ -2,7 +2,7 @@
|
||||||
title: 'Profile extend API を廃止して import + Lua 代入に寄せる'
|
title: 'Profile extend API を廃止して import + Lua 代入に寄せる'
|
||||||
state: 'inprogress'
|
state: 'inprogress'
|
||||||
created_at: '2026-06-13T07:31:09Z'
|
created_at: '2026-06-13T07:31:09Z'
|
||||||
updated_at: '2026-06-14T06:10:45Z'
|
updated_at: '2026-06-14T06:24:18Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
readiness: 'implementation_ready'
|
readiness: 'implementation_ready'
|
||||||
risk_flags: ['profiles', 'lua-api', 'builtin-resources', 'migration']
|
risk_flags: ['profiles', 'lua-api', 'builtin-resources', 'migration']
|
||||||
|
|
|
||||||
|
|
@ -82,4 +82,29 @@ Validation:
|
||||||
|
|
||||||
Ticket evidence, related records, orchestration plan, and clean workspace state were checked. No blockers remain; accept for implementation before worktree/spawn side effects.
|
Ticket evidence, related records, orchestration plan, and clean workspace state were checked. No blockers remain; accept for implementation before worktree/spawn side effects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-14T06:24:18Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implemented removal/deprecation of yoi.profile.extend and migrated builtin role Profile resources to yoi.profile.import plus explicit Lua assignment.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- yoi.profile.extend now fails with a removed-API diagnostic directing users to yoi.profile.import(...) plus assignment; the previous JSON deep-merge implementation was removed.
|
||||||
|
- Builtin role Lua profiles (companion/intake/orchestrator/coder/reviewer) import builtin:default, then assign each overridden field explicitly. Worker instruction overrides preserve imported worker defaults by assigning p.worker.instruction.
|
||||||
|
- Focused profile tests now cover explicit assignment, object replacement without hidden deep-merge retention, removed extend diagnostics, and role profile resolution/feature policy.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- cargo fmt --check
|
||||||
|
- git diff --check
|
||||||
|
- cargo test -p manifest profile::tests:: -- --nocapture
|
||||||
|
- cargo build -p yoi
|
||||||
|
- nix build .#yoi (completed; dirty-tree warning expected before commit, result symlink removed)
|
||||||
|
|
||||||
|
Risks/notes:
|
||||||
|
- yoi.profile.extend remains present only as a failing diagnostic stub so callers receive an actionable migration error instead of a nil-call Lua error.
|
||||||
|
- Scope/delegation authority semantics were not expanded; this change only preserves existing builtin profile scope/delegation declarations while changing composition style.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1054,12 +1054,10 @@ fn profile_module(lua: &Lua) -> mlua::Result<Table> {
|
||||||
)?;
|
)?;
|
||||||
module.set(
|
module.set(
|
||||||
"extend",
|
"extend",
|
||||||
lua.create_function(|lua, (reference, overrides): (String, LuaValue)| {
|
lua.create_function(|_, (_reference, _overrides): (String, LuaValue)| {
|
||||||
let base_value = import_profile_artifact(lua, &reference)?;
|
Err::<LuaValue, _>(mlua::Error::RuntimeError(
|
||||||
let mut base_json: serde_json::Value = lua.from_value(base_value)?;
|
"yoi.profile.extend has been removed; use yoi.profile.import(...) and explicit Lua assignment instead".to_string(),
|
||||||
let override_json: serde_json::Value = lua.from_value(overrides)?;
|
))
|
||||||
deep_merge_profile_json(&mut base_json, override_json);
|
|
||||||
lua.to_value(&base_json)
|
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
let meta = lua.create_table()?;
|
let meta = lua.create_table()?;
|
||||||
|
|
@ -1084,23 +1082,6 @@ fn builtin_profile_by_ref(reference: &str) -> Option<&'static BuiltinProfile> {
|
||||||
.iter()
|
.iter()
|
||||||
.find(|profile| profile.name == name || profile.label == reference)
|
.find(|profile| profile.name == name || profile.label == reference)
|
||||||
}
|
}
|
||||||
fn deep_merge_profile_json(base: &mut serde_json::Value, overrides: serde_json::Value) {
|
|
||||||
match (base, overrides) {
|
|
||||||
(serde_json::Value::Object(base), serde_json::Value::Object(overrides)) => {
|
|
||||||
for (key, value) in overrides {
|
|
||||||
match base.get_mut(&key) {
|
|
||||||
Some(existing) => deep_merge_profile_json(existing, value),
|
|
||||||
None => {
|
|
||||||
base.insert(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(base, override_value) => {
|
|
||||||
*base = override_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn models_module(lua: &Lua) -> mlua::Result<Table> {
|
fn models_module(lua: &Lua) -> mlua::Result<Table> {
|
||||||
let t = lua.create_table()?;
|
let t = lua.create_table()?;
|
||||||
t.set(
|
t.set(
|
||||||
|
|
@ -1610,9 +1591,9 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
let companion = resolve("companion");
|
let companion = resolve("companion");
|
||||||
assert!(!companion.feature.task.enabled);
|
assert!(companion.feature.task.enabled);
|
||||||
assert!(!companion.feature.pods.enabled);
|
assert!(companion.feature.pods.enabled);
|
||||||
assert!(!companion.feature.ticket.enabled);
|
assert!(companion.feature.ticket.enabled);
|
||||||
assert_eq!(companion.scope.allow[0].permission, Permission::Write);
|
assert_eq!(companion.scope.allow[0].permission, Permission::Write);
|
||||||
assert_eq!(companion.scope.deny.len(), 1);
|
assert_eq!(companion.scope.deny.len(), 1);
|
||||||
assert_eq!(companion.scope.deny[0].permission, Permission::Write);
|
assert_eq!(companion.scope.deny[0].permission, Permission::Write);
|
||||||
|
|
@ -1646,7 +1627,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let coder = resolve("coder");
|
let coder = resolve("coder");
|
||||||
assert!(!coder.feature.task.enabled);
|
assert!(coder.feature.task.enabled);
|
||||||
assert!(!coder.feature.pods.enabled);
|
assert!(!coder.feature.pods.enabled);
|
||||||
assert_eq!(coder.scope.allow[0].permission, Permission::Write);
|
assert_eq!(coder.scope.allow[0].permission, Permission::Write);
|
||||||
assert_eq!(coder.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
assert_eq!(coder.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
||||||
|
|
@ -1812,23 +1793,23 @@ return yoi.profile {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn global_yoi_import_and_extend_builtin_profile() {
|
fn global_yoi_import_supports_explicit_lua_assignment() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let profile = write_profile(
|
let profile = write_profile(
|
||||||
tmp.path(),
|
tmp.path(),
|
||||||
"extended.lua",
|
"assigned.lua",
|
||||||
r#"
|
r#"
|
||||||
local imported = yoi.profile.import("builtin:default")
|
local p = yoi.profile.import("builtin:default")
|
||||||
assert(imported.model.ref == "codex-oauth/gpt-5.5")
|
assert(p.model.ref == "codex-oauth/gpt-5.5")
|
||||||
return yoi.profile.extend("builtin:default", {
|
p.slug = "assigned"
|
||||||
slug = "extended",
|
p.model = yoi.models.catalog("anthropic/claude-sonnet-4-6")
|
||||||
model = yoi.models.catalog("anthropic/claude-sonnet-4-6"),
|
p.feature = {
|
||||||
feature = {
|
task = { enabled = false },
|
||||||
task = { enabled = false },
|
pods = { enabled = true },
|
||||||
pods = { enabled = true },
|
}
|
||||||
},
|
p.web = { enabled = false }
|
||||||
compaction = { kind = "tokens", threshold = 123, request_threshold = 456 },
|
p.compaction = yoi.compact.tokens { threshold = 123, request_threshold = 456 }
|
||||||
})
|
return p
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
let resolved = ProfileResolver::new()
|
let resolved = ProfileResolver::new()
|
||||||
|
|
@ -1844,25 +1825,27 @@ return yoi.profile.extend("builtin:default", {
|
||||||
);
|
);
|
||||||
assert!(!resolved.manifest.feature.task.enabled);
|
assert!(!resolved.manifest.feature.task.enabled);
|
||||||
assert!(resolved.manifest.feature.pods.enabled);
|
assert!(resolved.manifest.feature.pods.enabled);
|
||||||
|
assert_eq!(resolved.manifest.web.as_ref().unwrap().enabled, Some(false));
|
||||||
|
assert!(resolved.manifest.web.as_ref().unwrap().search.is_none());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved.manifest.compaction.as_ref().unwrap().threshold,
|
resolved.manifest.compaction.as_ref().unwrap().threshold,
|
||||||
Some(123)
|
Some(123)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved.profile.as_ref().unwrap().name.as_deref(),
|
resolved.profile.as_ref().unwrap().name.as_deref(),
|
||||||
Some("extended")
|
Some("assigned")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn global_yoi_extend_keeps_profile_validation_boundary() {
|
fn global_yoi_extend_fails_with_removed_api_diagnostic() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let profile = write_profile(
|
let profile = write_profile(
|
||||||
tmp.path(),
|
tmp.path(),
|
||||||
"bad.lua",
|
"bad.lua",
|
||||||
r#"
|
r#"
|
||||||
return yoi.profile.extend("builtin:default", {
|
return yoi.profile.extend("builtin:default", {
|
||||||
pod = { name = "not-runtime" },
|
slug = "bad",
|
||||||
})
|
})
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
@ -1873,7 +1856,10 @@ return yoi.profile.extend("builtin:default", {
|
||||||
ProfileResolveOptions::with_pod_name("p"),
|
ProfileResolveOptions::with_pod_name("p"),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(err.to_string().contains("field `pod`"));
|
let message = err.to_string();
|
||||||
|
assert!(message.contains("yoi.profile.extend"), "{message}");
|
||||||
|
assert!(message.contains("removed"), "{message}");
|
||||||
|
assert!(message.contains("yoi.profile.import"), "{message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
return yoi.profile.extend("builtin:default", {
|
local p = yoi.profile.import("builtin:default")
|
||||||
slug = "coder",
|
|
||||||
description = "Coder role profile with bundled reusable policy",
|
|
||||||
|
|
||||||
scope = yoi.scope.workspace_write(),
|
p.slug = "coder"
|
||||||
|
p.description = "Coder role profile with bundled reusable policy"
|
||||||
|
p.scope = yoi.scope.workspace_write()
|
||||||
|
p.worker.instruction = "$yoi/role/coder"
|
||||||
|
p.feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pods = { enabled = false },
|
||||||
|
ticket = { enabled = false, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
}
|
||||||
|
|
||||||
worker = {
|
return p
|
||||||
instruction = "$yoi/role/coder",
|
|
||||||
},
|
|
||||||
|
|
||||||
feature = {
|
|
||||||
task = { enabled = true },
|
|
||||||
memory = { enabled = true },
|
|
||||||
web = { enabled = true },
|
|
||||||
pods = { enabled = false },
|
|
||||||
ticket = { enabled = false, access = "lifecycle" },
|
|
||||||
ticket_orchestration = { enabled = false },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
return yoi.profile.extend("builtin:default", {
|
local p = yoi.profile.import("builtin:default")
|
||||||
slug = "companion",
|
|
||||||
description = "Companion role profile with bundled reusable policy",
|
|
||||||
|
|
||||||
scope = yoi.scope.workspace_write({
|
p.slug = "companion"
|
||||||
deny_write = { ".worktree" },
|
p.description = "Companion role profile with bundled reusable policy"
|
||||||
}),
|
p.scope = yoi.scope.workspace_write({
|
||||||
|
deny_write = { ".worktree" },
|
||||||
feature = {
|
|
||||||
task = { enabled = true },
|
|
||||||
memory = { enabled = true },
|
|
||||||
web = { enabled = true },
|
|
||||||
pods = { enabled = true },
|
|
||||||
ticket = { enabled = true, access = "lifecycle" },
|
|
||||||
ticket_orchestration = { enabled = false },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
p.feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pods = { enabled = true },
|
||||||
|
ticket = { enabled = true, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
return yoi.profile.extend("builtin:default", {
|
local p = yoi.profile.import("builtin:default")
|
||||||
slug = "intake",
|
|
||||||
description = "Intake role profile with bundled reusable policy",
|
|
||||||
|
|
||||||
scope = yoi.scope.workspace_read(),
|
p.slug = "intake"
|
||||||
|
p.description = "Intake role profile with bundled reusable policy"
|
||||||
|
p.scope = yoi.scope.workspace_read()
|
||||||
|
p.worker.instruction = "$yoi/role/intake"
|
||||||
|
p.feature = {
|
||||||
|
task = { enabled = false },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pods = { enabled = false },
|
||||||
|
ticket = { enabled = true, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
}
|
||||||
|
|
||||||
worker = {
|
return p
|
||||||
instruction = "$yoi/role/intake",
|
|
||||||
},
|
|
||||||
|
|
||||||
feature = {
|
|
||||||
task = { enabled = false },
|
|
||||||
memory = { enabled = true },
|
|
||||||
web = { enabled = true },
|
|
||||||
pods = { enabled = false },
|
|
||||||
ticket = { enabled = true, access = "lifecycle" },
|
|
||||||
ticket_orchestration = { enabled = false },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
return yoi.profile.extend("builtin:default", {
|
local p = yoi.profile.import("builtin:default")
|
||||||
slug = "orchestrator",
|
|
||||||
description = "Orchestrator role profile with bundled reusable policy",
|
|
||||||
|
|
||||||
scope = "workspace_read",
|
p.slug = "orchestrator"
|
||||||
|
p.description = "Orchestrator role profile with bundled reusable policy"
|
||||||
|
p.scope = "workspace_read"
|
||||||
|
p.worker.instruction = "$yoi/role/orchestrator"
|
||||||
|
p.feature = {
|
||||||
|
task = { enabled = false },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pods = { enabled = true },
|
||||||
|
ticket = { enabled = true, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = true },
|
||||||
|
}
|
||||||
|
p.delegation_scope = "workspace_write"
|
||||||
|
|
||||||
worker = {
|
return p
|
||||||
instruction = "$yoi/role/orchestrator",
|
|
||||||
},
|
|
||||||
|
|
||||||
feature = {
|
|
||||||
task = { enabled = false },
|
|
||||||
memory = { enabled = true },
|
|
||||||
web = { enabled = true },
|
|
||||||
pods = { enabled = true },
|
|
||||||
ticket = { enabled = true, access = "lifecycle" },
|
|
||||||
ticket_orchestration = { enabled = true },
|
|
||||||
},
|
|
||||||
|
|
||||||
delegation_scope = "workspace_write",
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
return yoi.profile.extend("builtin:default", {
|
local p = yoi.profile.import("builtin:default")
|
||||||
slug = "reviewer",
|
|
||||||
description = "Reviewer role profile with bundled reusable policy",
|
|
||||||
|
|
||||||
scope = yoi.scope.workspace_read(),
|
p.slug = "reviewer"
|
||||||
|
p.description = "Reviewer role profile with bundled reusable policy"
|
||||||
|
p.scope = yoi.scope.workspace_read()
|
||||||
|
p.worker.instruction = "$yoi/role/reviewer"
|
||||||
|
p.feature = {
|
||||||
|
task = { enabled = false },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pods = { enabled = false },
|
||||||
|
ticket = { enabled = false, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
}
|
||||||
|
|
||||||
worker = {
|
return p
|
||||||
instruction = "$yoi/role/reviewer",
|
|
||||||
},
|
|
||||||
|
|
||||||
feature = {
|
|
||||||
task = { enabled = false },
|
|
||||||
memory = { enabled = true },
|
|
||||||
web = { enabled = true },
|
|
||||||
pods = { enabled = false },
|
|
||||||
ticket = { enabled = false, access = "lifecycle" },
|
|
||||||
ticket_orchestration = { enabled = false },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user