From e24fbabe54ca01f5c89557e2abf9ef16dcadb818 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 02:51:29 +0900 Subject: [PATCH] fix: align Profile authoring with compaction and reasoning contracts (T-710) Resolve MR seq9 supported-compaction-fields and numeric-extraction-reasoning findings. Cover all canonical CompactionConfigPartial settings, token aliases and ratio helpers; accept string effort and signed integer extraction budgets without materializing editor defaults. Audit the remaining Profile field groups against their real serde contracts. Share an actual-provider value corpus across native, generated WASM, and shared save/projection/verified Runtime archive consumption. Prove inline/import/patch exact values and effective Manifest settings; gate canonical compaction field coverage against serialization of the actual partial type. Keep prior scope union and worker/feature diagnostics fixes intact. Validation: root cargo check; config-source40; unfiltered server644+11; Runtime340+17+17; API typescript77; Deno39; deno task check0/0; wasm32 release regeneration byte-identical with wasm-bindgen0.2.117/Binaryen132; cargo fmt/diff checks. Logs target/t710-contract-{check,config-source,server,api,runtime}.log. Reviewer seq9 independently confirmed seq3/5/7 resolution and correct main/relative-cwd routing. No target integration/ref mutations. --- crates/config-source/src/lib.rs | 104 ++++++++++++++ .../src/server/tests/value_profiles_tests.rs | 135 ++++++++++++++++++ docs/design/profiles-manifests-prompts.md | 2 + .../profile-authoring-values.json | 116 +++++++++++++++ .../config-schema/profile-authoring.dcdl | 17 ++- .../test/config-source/wasm-parity.test.ts | 95 ++++++++++++ 6 files changed, 466 insertions(+), 3 deletions(-) create mode 100644 resources/config-schema/profile-authoring-values.json diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index d5c1f7eb5..71264ef60 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -2738,6 +2738,110 @@ mod tests { } } + fn fixture_decodal(value: &serde_json::Value) -> String { + match value { + serde_json::Value::Object(fields) => format!( + "{{ {} }}", + fields + .iter() + .map(|(key, value)| format!("{key} = {};", fixture_decodal(value))) + .collect::>() + .join(" ") + ), + serde_json::Value::Array(values) => format!( + "[{}]", + values + .iter() + .map(fixture_decodal) + .collect::>() + .join(", ") + ), + serde_json::Value::Null => panic!("fixture omission must not be represented as null"), + value => value.to_string(), + } + } + + #[test] + fn actual_profile_authoring_value_corpus_preserves_supported_forms() { + let cases: Vec = serde_json::from_str(include_str!( + "../../../resources/config-schema/profile-authoring-values.json" + )) + .unwrap(); + for case in cases { + let recipe = fixture_decodal(&case["profile"]); + for (form, patched) in [ + (recipe.clone(), false), + (r#"import "./recipe.dcdl""#.into(), false), + ( + r#"(import "./recipe.dcdl") // { description = "patched"; }"#.into(), + true, + ), + ] { + let source = format!( + r#"{{ profile.entries = [{{ selector = "project:corpus"; profile = {form}; }}]; }} as WorkspaceConfigSchema"# + ); + let snapshot = ConfigTreeSnapshot::from_entries( + 10, + [entry("main.dcdl", &source), entry("recipe.dcdl", &recipe)], + ) + .unwrap(); + let schema = actual_profile_schema(); + let environment = + SnapshotEnvironment::new(snapshot).with_schema_bundle(schema.clone()); + let contract = + ToolchainContract::with_schema_bundle(1, vec![path("main.dcdl")], 1, schema); + let before = environment.evaluate_contract(&contract).unwrap(); + let diagnostics = environment.analyze(&path("main.dcdl"), None); + assert!( + diagnostics.is_empty(), + "{}: {form}: {diagnostics:?}", + case["name"] + ); + let mut expected = case["profile"].clone(); + if patched { + expected["description"] = serde_json::json!("patched"); + } + assert_eq!( + before.projections[0] + .data_json + .pointer("/profile/entries/0/profile"), + Some(&expected), + "{}", + case["name"] + ); + assert_eq!(environment.evaluate_contract(&contract).unwrap(), before); + } + } + let source = "{} as WorkspaceConfigSchema"; + let environment = SnapshotEnvironment::new( + ConfigTreeSnapshot::from_entries(10, [entry("main.dcdl", source)]).unwrap(), + ) + .with_schema_bundle(actual_profile_schema()); + for (profile_path, prefix, label) in [ + ("compaction", "reta", "retained_tokens"), + ("compaction", "prune_min", "prune_min_savings"), + ("compaction", "req", "request"), + ("compaction", "wor", "worker"), + ("compaction", "compact_ret", "compact_retained_tokens"), + ("feature.memory.extraction", "reas", "reasoning"), + ("feature.subjektiv.extraction", "reas", "reasoning"), + ] { + let marked = format!( + "{{ profile.entries = [{{ profile.{profile_path} = {{ {prefix}| }}; }}]; }} as WorkspaceConfigSchema" + ); + let cursor = marked.find('|').unwrap(); + let source = marked.replace('|', ""); + let result = environment + .complete_config(&path("main.dcdl"), &source, cursor, true) + .unwrap() + .unwrap(); + assert!( + result.items.iter().any(|item| item.label == label), + "{source}: {result:?}" + ); + } + } + #[test] fn completion_authoring_sources_compose_with_validation_source_fallbacks() { let schema = WorkspaceConfigSchemaBundle::compose([ diff --git a/crates/workspace-server/src/server/tests/value_profiles_tests.rs b/crates/workspace-server/src/server/tests/value_profiles_tests.rs index 72c72f6ad..b375fdeb9 100644 --- a/crates/workspace-server/src/server/tests/value_profiles_tests.rs +++ b/crates/workspace-server/src/server/tests/value_profiles_tests.rs @@ -303,6 +303,141 @@ async fn value_profiles_scalar_and_table_scopes_analyze_save_and_resolve_equally assert_eq!(scopes[1], scopes[2]); } +fn profile_fixture_decodal(value: &serde_json::Value) -> String { + match value { + serde_json::Value::Object(fields) => format!( + "{{ {} }}", + fields + .iter() + .map(|(key, value)| format!("{key} = {};", profile_fixture_decodal(value))) + .collect::>() + .join(" ") + ), + serde_json::Value::Array(values) => format!( + "[{}]", + values + .iter() + .map(profile_fixture_decodal) + .collect::>() + .join(", ") + ), + serde_json::Value::Null => panic!("fixture omission must not be represented as null"), + value => value.to_string(), + } +} + +#[tokio::test] +async fn value_profiles_authoring_corpus_preserves_effective_runtime_settings() { + let cases: Vec = serde_json::from_str(include_str!( + "../../../../../resources/config-schema/profile-authoring-values.json" + )) + .unwrap(); + // The canonical token fixture must exercise every serializable setting, + // so adding a field to CompactionConfigPartial cannot silently outgrow the + // editor-shape coverage. Aliases/ratio helpers have separate corpus cases. + let default = serde_json::to_value(manifest::CompactionConfigPartial::default()).unwrap(); + let canonical = cases + .iter() + .find(|case| case["name"] == "canonical_compaction_tokens") + .unwrap(); + assert_eq!( + default + .as_object() + .unwrap() + .keys() + .collect::>(), + canonical["profile"]["compaction"] + .as_object() + .unwrap() + .keys() + .filter(|name| name.as_str() != "kind") + .collect() + ); + for case in cases { + let dir = tempfile::tempdir().unwrap(); + let api = test_api(dir.path()).await; + let recipe = profile_fixture_decodal(&case["profile"]); + for (index, (form, patched)) in [ + (recipe.clone(), false), + (r#"import "./recipe.dcdl""#.into(), false), + ( + r#"(import "./recipe.dcdl") // { description = "patched"; }"#.into(), + true, + ), + ] + .iter() + .enumerate() + { + let extra = if index == 0 { + vec![config_source::ConfigTreeChange::Create { + path: config_source::VirtualPath::parse("recipe.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: recipe.clone(), + }] + } else { + vec![] + }; + let authored = format!("{} as WorkspaceConfigSchema", source(form)); + let state = commit_workspace_config_tree( + &api, + TEST_WORKSPACE_ID, + &request(&api, &authored, extra), + ) + .unwrap(); + let environment = config_source::SnapshotEnvironment::new(state.snapshot.clone()) + .with_schema_bundle(state.contract.schema_bundle.clone()); + let diagnostics = environment.analyze( + &config_source::VirtualPath::parse("main.dcdl").unwrap(), + None, + ); + assert!(diagnostics.is_empty(), "{}: {diagnostics:?}", case["name"]); + let mut expected = case["profile"].clone(); + if *patched { + expected["description"] = serde_json::json!("patched"); + } + let evaluated = environment.evaluate_contract(&state.contract).unwrap(); + assert_eq!( + evaluated.projections[0] + .data_json + .pointer("/profile/entries/0/profile"), + Some(&expected) + ); + let projection = + project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &state).unwrap(); + let bundle = build_virtual_profile_config_bundle( + &projection, + &state, + TEST_WORKSPACE_ID, + "created", + "project:alpha", + ) + .unwrap() + .unwrap(); + let resolved = bundle + .profile_source_archive + .as_ref() + .unwrap() + .verify() + .unwrap() + .resolve_profile( + "project:alpha", + Path::new("/runtime-worker"), + "corpus-worker", + ) + .unwrap(); + let manifest = serde_json::to_value(resolved).unwrap(); + for (pointer, value) in case["manifest_expect"].as_object().unwrap() { + assert_eq!( + manifest.pointer(pointer), + Some(value), + "{}: {pointer}", + case["name"] + ); + } + } + } +} + #[tokio::test] async fn value_profiles_invalid_saves_preserve_the_active_config() { let dir = tempfile::tempdir().unwrap(); diff --git a/docs/design/profiles-manifests-prompts.md b/docs/design/profiles-manifests-prompts.md index f89c33192..f791eb084 100644 --- a/docs/design/profiles-manifests-prompts.md +++ b/docs/design/profiles-manifests-prompts.md @@ -47,6 +47,8 @@ The schema contribution separates its materialized registration schema from an o `scope` and `delegation_scope` retain both supported forms: a string such as `"workspace_read"` / `"workspace_write"`, or a table such as `{ intent = "workspace_read"; deny_write = ["private"]; symlink_policy = "resolved"; }`. Decodal 0.4 has no union range, so these fields are opaque to the editor constraint pass; a non-materialized table hint supplies nested completion without rejecting the string form or rewriting it. The shared Backend save validator checks scope forms, intent values and table fields (including their types and unknown keys). Other structurally typed fields such as `worker.mode` and `feature.task.enabled` retain editor type diagnostics. Completion hints never become Profile defaults or saved values. +Extraction `reasoning` in both `feature.memory` and `feature.subjektiv`, like `engine.reasoning`, accepts either a string effort or a signed integer budget. These union values are also opaque to editor constraints and semantically checked on save. Compaction authoring covers the complete token settings and their supported `compact_*` aliases, plus the ratio helper's `request` / `worker` aliases; values are not renamed or defaulted during Workspace evaluation. The shared test corpus `resources/config-schema/profile-authoring-values.json` proves exact values in native/WASM analysis and effective settings through save/projection/Runtime consumption. Its canonical compaction case must cover every serialized `CompactionConfigPartial` field, making future contract drift visible in tests. + ## Manifests A resolved Manifest is the concrete contract used to create or restore a Worker. It carries defaults, resolved paths, permissions, scope, prompt references, provider/model decisions, and runtime identity. diff --git a/resources/config-schema/profile-authoring-values.json b/resources/config-schema/profile-authoring-values.json new file mode 100644 index 000000000..4038f72f4 --- /dev/null +++ b/resources/config-schema/profile-authoring-values.json @@ -0,0 +1,116 @@ +[ + { + "name": "canonical_compaction_tokens", + "profile": { + "compaction": { + "kind": "tokens", + "prune_enabled": false, + "prune_protected_tokens": 3000, + "prune_min_savings": 1000, + "threshold": 80000, + "request_threshold": 90000, + "retained_tokens": 12000, + "overview_target_tokens": 1000, + "overview_warning_tokens": 2000, + "overview_deadline_tokens": 3000, + "worker_context_max_tokens": 20000, + "finish_warning_remaining_tokens": 1000, + "final_reserve_tokens": 2000, + "worker_max_turns": 5, + "summary_target_tokens": 1000, + "summary_max_tokens": 2000, + "auto_read_budget_tokens": 500, + "result_context_max_tokens": 4000, + "model": { "ref": "codex-oauth/gpt-5.6-sol" } + } + }, + "manifest_expect": { + "/compaction/prune_enabled": false, + "/compaction/prune_protected_tokens": 3000, + "/compaction/prune_min_savings": 1000, + "/compaction/threshold": 80000, + "/compaction/request_threshold": 90000, + "/compaction/retained_tokens": 12000, + "/compaction/overview_target_tokens": 1000, + "/compaction/overview_warning_tokens": 2000, + "/compaction/overview_deadline_tokens": 3000, + "/compaction/worker_context_max_tokens": 20000, + "/compaction/finish_warning_remaining_tokens": 1000, + "/compaction/final_reserve_tokens": 2000, + "/compaction/worker_max_turns": 5, + "/compaction/summary_target_tokens": 1000, + "/compaction/summary_max_tokens": 2000, + "/compaction/auto_read_budget_tokens": 500, + "/compaction/result_context_max_tokens": 4000, + "/compaction/model/ref": "codex-oauth/gpt-5.6-sol" + } + }, + { + "name": "compaction_token_aliases_without_kind", + "profile": { + "compaction": { + "compact_threshold": 80000, + "compact_request_threshold": 90000, + "compact_retained_tokens": 12000, + "compact_worker_max_input_tokens": 20000, + "compact_worker_max_turns": 5, + "compact_auto_read_budget": 500 + } + }, + "manifest_expect": { + "/compaction/threshold": 80000, + "/compaction/request_threshold": 90000, + "/compaction/retained_tokens": 12000, + "/compaction/worker_context_max_tokens": 20000, + "/compaction/worker_max_turns": 5, + "/compaction/auto_read_budget_tokens": 500 + } + }, + { + "name": "compaction_ratio_aliases", + "profile": { + "model": { "context_window": 100000 }, + "compaction": { + "kind": "ratio", + "threshold": 0.9, + "request": 0.8, + "worker": 0.2 + } + }, + "manifest_expect": { + "/compaction/threshold": 90000, + "/compaction/request_threshold": 80000, + "/compaction/worker_context_max_tokens": 20000 + } + }, + { + "name": "numeric_extraction_reasoning", + "profile": { + "engine": { "reasoning": -1 }, + "feature": { + "memory": { "extraction": { "enabled": true, "reasoning": 4096 } }, + "subjektiv": { "extraction": { "enabled": true, "reasoning": 4096 } } + } + }, + "manifest_expect": { + "/engine/reasoning": -1, + "/feature/memory/profile/extraction/reasoning": 4096, + "/feature/subjektiv/profile/extraction/reasoning": 4096 + } + }, + { + "name": "string_extraction_reasoning", + "profile": { + "feature": { + "memory": { "extraction": { "enabled": true, "reasoning": "high" } }, + "subjektiv": { + "extraction": { "enabled": true, "reasoning": "medium" } + } + } + }, + "manifest_expect": { + "/feature/memory/profile/extraction/reasoning": "high", + "/feature/subjektiv/profile/extraction/reasoning": "medium" + } + } +] diff --git a/resources/config-schema/profile-authoring.dcdl b/resources/config-schema/profile-authoring.dcdl index ffd1b5e83..d2cb97ce3 100644 --- a/resources/config-schema/profile-authoring.dcdl +++ b/resources/config-schema/profile-authoring.dcdl @@ -9,7 +9,8 @@ let capability = Unknown; context_window = Int; max_context_window = Int; }; Extraction = { - enabled = Bool; model = Model; reasoning = String; + # ReasoningControl accepts string effort or signed integer budget. + enabled = Bool; model = Model; reasoning = Unknown; threshold = Int; worker_max_turns = Int; }; # Profile scope accepts either a string intent or a table. Decodal 0.4 has @@ -56,9 +57,19 @@ let env = { inherit = [...String]; set = { ...Unknown }; }; }]; }; compaction = { + # Token settings (CompactionConfigPartial) and ratio helper fields. + # Threshold/context values accept integer tokens or numeric ratios. kind = String; threshold = Unknown; request_threshold = Unknown; - prune_enabled = Bool; worker_context_max_tokens = Unknown; - worker_max_turns = Int; model = Model; + worker_context_max_tokens = Unknown; request = Unknown; worker = Unknown; + prune_enabled = Bool; prune_protected_tokens = Int; prune_min_savings = Int; + retained_tokens = Int; + overview_target_tokens = Int; overview_warning_tokens = Int; overview_deadline_tokens = Int; + finish_warning_remaining_tokens = Int; final_reserve_tokens = Int; + worker_max_turns = Int; summary_target_tokens = Int; summary_max_tokens = Int; + auto_read_budget_tokens = Int; result_context_max_tokens = Int; model = Model; + # Supported token aliases remain authorable without rewriting recipes. + compact_threshold = Int; compact_request_threshold = Int; compact_retained_tokens = Int; + compact_worker_max_input_tokens = Int; compact_worker_max_turns = Int; compact_auto_read_budget = Int; }; web = { enabled = Bool; allow_private_addresses = Bool; diff --git a/web/workspace/test/config-source/wasm-parity.test.ts b/web/workspace/test/config-source/wasm-parity.test.ts index be59ab095..905303f1d 100644 --- a/web/workspace/test/config-source/wasm-parity.test.ts +++ b/web/workspace/test/config-source/wasm-parity.test.ts @@ -382,6 +382,22 @@ Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () => assertEquals(result.items.some((item) => item.label === "profile"), true); }); +function fixtureDecodal(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(fixtureDecodal).join(", ")}]`; + if (value !== null && typeof value === "object") { + return `{ ${ + Object.entries(value).map(([key, value]) => + `${key} = ${fixtureDecodal(value)};` + ).join(" ") + } }`; + } + if ( + typeof value === "string" || typeof value === "number" || + typeof value === "boolean" + ) return JSON.stringify(value); + throw new Error("fixture omission must not be represented as null/undefined"); +} + Deno.test("generated WASM authors value-based Profiles using the Backend schema without materializing authoring fields", async () => { const source = await Deno.readTextFile( new URL( @@ -656,6 +672,85 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema ); } } + const cases = JSON.parse( + await Deno.readTextFile( + new URL( + "../../../../resources/config-schema/profile-authoring-values.json", + import.meta.url, + ), + ), + ) as Array<{ name: string; profile: Record }>; + for (const fixture of cases) { + const recipe = fixtureDecodal(fixture.profile); + for ( + const [form, patched] of [[recipe, false], [ + 'import "./recipe.dcdl"', + false, + ], [ + '(import "./recipe.dcdl") // { description = "patched"; }', + true, + ]] as const + ) { + const fixtureContent = content.replace( + "profile = {};", + `profile = ${form};`, + ); + const fixtureTree = { + ...tree, + entries: { + "main.dcdl": { + ...tree.entries["main.dcdl"], + content: fixtureContent, + content_digest: await digestText(fixtureContent), + }, + "recipe.dcdl": { + path: "recipe.dcdl", + content_type: "decodal", + content: recipe, + content_digest: await digestText(recipe), + }, + }, + }; + assertEquals( + analyze_snapshot(fixtureTree, "main.dcdl", undefined), + [], + `${fixture.name}: ${form}`, + ); + const result = evaluate_snapshot( + fixtureTree, + valueContract, + ) as typeof evaluated; + assertEquals( + result.projections[0].data_json.profile.entries[0].profile, + patched + ? { ...fixture.profile, description: "patched" } + : fixture.profile, + ); + } + } + for ( + const [path, prefix, label] of [ + ["compaction", "reta", "retained_tokens"], + ["compaction", "prune_min", "prune_min_savings"], + ["compaction", "req", "request"], + ["compaction", "wor", "worker"], + ["compaction", "compact_ret", "compact_retained_tokens"], + ["feature.memory.extraction", "reas", "reasoning"], + ["feature.subjektiv.extraction", "reas", "reasoning"], + ] + ) { + const body = + `{ profile.entries = [{ profile.${path} = { ${prefix} } }]; } as WorkspaceConfigSchema`; + const cursor = body.lastIndexOf(prefix) + prefix.length; + const result = complete_current("main.dcdl", body, cursor, true) as { + items: Array<{ label: string }>; + }; + assertEquals( + result.items.some((item) => item.label === label), + true, + `${path}.${label}`, + ); + } // Neither completion nor diagnostic shape checking changes the saved value. assertEquals(evaluate_snapshot(tree, valueContract), evaluated); });